[譯] 使用 Architecture Components 開(kāi)發(fā) MVVM 應(yīng)用:MVP 開(kāi)發(fā)者的實(shí)踐指南

原文:https://antonioleiva.com/mvvm-vs-mvp/
作者:https://antonioleiva.com/

譯者說(shuō)

最近在學(xué)習(xí) MVVM 相關(guān)的知識(shí)粤攒,在最新一期的 KotlinWeekly 發(fā)現(xiàn)了這篇文章。作者通過(guò)循序漸進(jìn)的方式囱持,向我們闡述如何實(shí)現(xiàn) MVVM夯接,以及如何使用 Android Jetpack Components 組件來(lái)構(gòu)建 MVVM 應(yīng)用。讀完以后纷妆,收獲頗豐盔几。為了讓更多的開(kāi)發(fā)者了解到 MVVM,我斗膽翻譯過(guò)來(lái)掩幢,這便是這篇文章的來(lái)由逊拍。英語(yǔ)渣渣,如有錯(cuò)誤际邻,還請(qǐng)指正芯丧。

正文


導(dǎo)語(yǔ)

自從 Google 正式發(fā)布了 Android Jetpack Components 架構(gòu)組件,MVVM 已然成為了 Android Apps 官宣的主流開(kāi)發(fā)模式世曾。我認(rèn)為是時(shí)候注整,提供一些行之有效的幫助,幫助使用 Mvp 模式的開(kāi)發(fā)者來(lái)理解 MVVM 模式。

如果您碰巧看到這篇博客肿轨,但是不知道怎么在 Android 中使用 Mvp 模式,推薦您查看我之前寫(xiě)的關(guān)于 Mvp 的博客蕊程。

MVVM vs Mvp - 我需要去重構(gòu)我的 App 嗎椒袍?

在相當(dāng)長(zhǎng)的一段時(shí)間內(nèi),Mvp 似乎是用來(lái) 降低 UI 渲染業(yè)務(wù)邏輯 之間耦合的最受歡迎的開(kāi)發(fā)模式藻茂。但是驹暑,現(xiàn)在我們有了新的選擇。

許多開(kāi)發(fā)者詢(xún)問(wèn)我辨赐,是否應(yīng)該逃避 Mvp优俘,或者當(dāng)開(kāi)始新的項(xiàng)目如何設(shè)計(jì)架構(gòu)。下面是一些想法:

  • Mvp 沒(méi)有消失掀序。它仍然是完全有效的開(kāi)發(fā)模式帆焕,如果您之前使用它,也可以接著使用不恭。
  • MVVM 作為新的開(kāi)發(fā)模式叶雹,不一定更好。但谷歌所做的具體實(shí)施是很有道理的换吧,之前使用 MVP 的原因是:它與 Android 框架非常吻合折晦,并且上手難度不大。
  • 使用 Mvp 并不意味著沾瓦,你不可以使用 Android Jetpack Components 架構(gòu)組件满着。可能 ViewModel 沒(méi)有多大的作用(它是 Presenter 的替代者)贯莺,但是其他組件可以在項(xiàng)目中使用风喇。
  • 您不需要立即重構(gòu)您的 App,如果您對(duì) Mvp 非常滿(mǎn)意乖篷,請(qǐng)繼續(xù)享受它响驴。一般來(lái)說(shuō),最好保持一個(gè)安全撕蔼,可靠的架構(gòu)豁鲤。而不是在項(xiàng)目中使用新的技術(shù)棧,畢竟重構(gòu)是需要成本的鲸沮。

MVVM 和 MVp 的差異

幸運(yùn)的是琳骡,如果您之前熟悉 Mvp,學(xué)習(xí) MVVM 將非常容易讼溺!在 Android 開(kāi)發(fā)中楣号,兩者只有一點(diǎn)點(diǎn)的差異:

在 Mvp 中,PresenterView 通過(guò) 接口 聯(lián)系。
在 MVVM 中炫狱,ViewModelView 通過(guò) 觀察者模式 通信藻懒。

我知道,如果你曾閱讀過(guò)維基百科關(guān)于 MVVM 的定義视译。將會(huì)發(fā)現(xiàn)和我之前所說(shuō)的完全不符嬉荆。但是在 Android 開(kāi)發(fā)領(lǐng)域中,拋開(kāi) Databinding 不談酷含,在我看來(lái)鄙早,這將是理解 MVVM 的最佳方式。

在不使用 Arch Components 的情況下椅亚,從 MVp 遷移至 MVVM

我將使用 MVVM 來(lái)改造之前的 androidmvp 例子限番,MVVM 示例代碼請(qǐng)戳這里 androidmvvm

我暫時(shí)不使用 Architecture Components呀舔,先自己實(shí)現(xiàn)弥虐。之后我們就可以清晰的認(rèn)識(shí)到 Google 新推出的 Android Jetpack Components 是如何工作的,以及如何讓開(kāi)發(fā)變得更加高效别威。

創(chuàng)建一個(gè) Observable 類(lèi)

當(dāng)我們使用 Observable 模式時(shí)躯舔,需要一個(gè)可以觀察的類(lèi)。該類(lèi)將持有 Observer 和將發(fā)送給 Observer 的泛型類(lèi)型的值省古, 以及當(dāng)值發(fā)生改變粥庄,通知到 Observer

class Observable<T> {

    private var observers = emptyList<(T) -> Unit>()

    fun addObserver(observer: (T) -> Unit) {
        observers += observer
    }

    fun clearObservers() {
        observers = emptyList()
    }

    fun callObservers(newValue: T) {
        observers.forEach {
            it(newValue)
        }
    }
}

使用 States 來(lái)表示 UI 更改

由于我們現(xiàn)在無(wú)法直接與 View 進(jìn)行通信豺妓,View 也不知道該怎么顯示惜互。我發(fā)現(xiàn)一個(gè)靈活的方式,通過(guò)一個(gè) Model 類(lèi)來(lái)表示 UI 狀態(tài)琳拭。

舉個(gè)栗子训堆,如果我們希望界面顯示一個(gè)進(jìn)度條,我們將發(fā)送一個(gè) Loading 狀態(tài)白嘁,消費(fèi)該狀態(tài)的方式完全由視圖決定坑鱼。

對(duì)于這種特殊情況,我創(chuàng)建了一個(gè) ScreenState 類(lèi)絮缅,它接受一個(gè)表示視圖所需狀態(tài)的泛型類(lèi)型鲁沥。

每個(gè)界面都有一些共同的狀態(tài),例如 Loading耕魄,Erroor画恰。然后是每個(gè)界面顯示的具體狀態(tài)。

可以使用以下密閉類(lèi)吸奴,來(lái)表示通用的 ScreenState

sealed class ScreenState<out T>{
    object Loading:ScreenState<Nothing>()
    class Render<T>(val renderState:T):ScreenState<T>()
}

對(duì)于特定狀態(tài)允扇,我們可能需要額外的定義缠局。對(duì)于登陸狀態(tài),枚舉類(lèi)就足夠了考润。

enum class LoginState{
    Success,
    WrongUserName,
    WrongUserPassword
}

但是對(duì)于 MainState狭园,我們正在顯示列表和消息,枚舉類(lèi)無(wú)法提供足夠的支持额划,所以密閉類(lèi)再次獲得我的青睞(稍后會(huì)看到具體原因)妙啃。

sealed class MainState{
    class ShowItems(val items:List<String>):MainState()
    class showMessage(val items:String):MainState()
}

將 Presenter 轉(zhuǎn)換為 ViewModel

我們不再需要定義 View 接口,你可以擺脫它俊戳。因?yàn)槲覀儗⑹褂?Observable 替代。

如下示例:

val stateObservable = Observable<ScreenState<LoginState>>()

之后馆匿,當(dāng)我們想顯示進(jìn)度條表示加載狀態(tài)時(shí)抑胎,只需要調(diào)用 LoadingStateObserver

fun validateCredentials(username: String, password: String) {
    stateObservable.callObservers(ScreenState.Loading)
    loginInteractor.login(username, password, this)
}

當(dāng)?shù)卿浲瓿蓵r(shí)渐北,需要展示成功信息:

override fun onSuccess() {
 stateObservable.callObservers(ScreenState.Render(LoginState.Success))
}

老實(shí)說(shuō)阿逃,登錄成功的狀態(tài)可以用不同的方式實(shí)現(xiàn),如果我們想要更明確赃蛛,可以使用 LoginState.NavigateToMain 或者類(lèi)似的方式進(jìn)入首頁(yè)恃锉。

但這取決于更多因素,取決于應(yīng)用程序架構(gòu)呕臂。我會(huì)這樣做破托。

然后,在 ViewModelonDestroy() 中歧蒋,我們清除了 Observers土砂,避免潛在的內(nèi)存泄漏問(wèn)題。

在 Activity 中使用 ViewModel

目前 Activity 還無(wú)法充當(dāng) ViewModel 中 View 的角色谜洽,因此 觀察者模式 將會(huì)受到重用萝映。

首先,初始化 ViewModel

private val viewModel = LoginViewModel(LoginInteractor())

之后阐虚,在 onCreate() 中觀察狀態(tài)序臂,當(dāng)狀態(tài)發(fā)生變化,將會(huì)調(diào)用 updateUI()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        viewModel.stateObservable.addObserver { updateUI() }
    }

在這里实束,感謝密閉類(lèi)和枚舉類(lèi)奥秆。通過(guò)使用 when 表達(dá)式,一些變得如此簡(jiǎn)單磕洪。我分兩步處理狀態(tài):首先是一般狀態(tài)吭练,然后是特定的 LoginState

第一個(gè) when 表達(dá)式分支:顯示加載狀態(tài)的進(jìn)度條析显。如果是其它特定狀態(tài)鲫咽,需要調(diào)用另外的函數(shù)處理签赃。

private fun updateUI(it: ScreenState<LoginState>) {
        when (it) {
            ScreenState.Loading -> progressbar.visibility = View.VISIBLE
            is ScreenState.Render -> processLoginState(it.renderState)
        }
    }

第二個(gè) when 表達(dá)式分支:首先隱藏進(jìn)度條(如果可見(jiàn)),如果是成功狀態(tài)分尸,則進(jìn)入首頁(yè)锦聊。如果是錯(cuò)誤狀態(tài),則提示相應(yīng)的錯(cuò)誤信息

private fun processLoginState(renderState: LoginState) {
        progressbar.visibility = View.GONE
        when (renderState) {
            LoginState.Success -> startActivity(Intent(this, MainActivity::class.java))
            LoginState.WrongUserName -> username.error = getString(R.string.username_error)
            LoginState.WrongUserPassword -> password.error = getString(R.string.password_error)
        }
    }

當(dāng)點(diǎn)擊登錄按鈕箩绍,調(diào)用 ViewModel 中的 onLoginClicked() 進(jìn)行操作孔庭。

 private fun login() {
        viewModel.onLoginClicked(username.text.toString(), password.text.toString())
    }

然后,在 Activity 中的 onDestroy() 調(diào)用 ViewModelonDestroy() 釋放資源(這樣就可以分離觀察者)材蛛。

override fun onDestroy() {
        viewModel.onDestroy()
        super.onDestroy()
    }

使用 Architecture Components 修改代碼

通過(guò)之前自己實(shí)現(xiàn) MVVM 的 ViewModel圆到,以便您可以輕松的看到差異。到目前為止卑吭,與 MVP 相比芽淡,MVVM 并沒(méi)有帶來(lái)更多的好處。

但也要一些不同豆赏,最重要的一點(diǎn)是您可以忘記 Activity 的銷(xiāo)毀挣菲,所以您可以脫離它的生命周期,隨時(shí)做你的工作掷邦。特別感謝 ViewModelLiveData白胀。當(dāng) Activity 重新創(chuàng)建或者被銷(xiāo)毀時(shí),您無(wú)需擔(dān)心應(yīng)用的崩潰抚岗。

這是工作原理:當(dāng) Activity 被重新創(chuàng)建或杠,ViewModel 仍然存在,當(dāng) Activity 被永久殺死的時(shí)候苟跪,將會(huì)調(diào)用 ViewModelonCleared()

viewmodel-lifecycle.png

由于 LiveData 也具有生命周期意識(shí)廷痘,因此它知道何時(shí)跟 LifecycleOwner 建立和斷開(kāi)聯(lián)系。所以您無(wú)需關(guān)心它件已。

我并不打算深入講解 Architecture Components 的工作原理(因?yàn)樵诠俜降拈_(kāi)發(fā)者指南中有更深刻的解釋?zhuān)┧穸睿宰屛覀兝^續(xù)探索實(shí)現(xiàn) MVVM

在項(xiàng)目中使用 Architecture Components篷扩,需要添加以下依賴(lài)

    implementation "android.arch.lifecycle:extensions:1.1.1"

如果您使用其他組件兄猩,如:Room 〖矗或者在 AndroidX 上使用這些組件枢冤,更多內(nèi)容請(qǐng)參考 這里

Architecture Components ViewModel

使用 ViewModel 非常簡(jiǎn)單铜秆,你只需要繼承 ViewModel 即可淹真。

class LoginViewModel(private val loginInteractor: LoginInteractor) : ViewModel()

刪除 onDestroy(),因?yàn)樗辉傩枰肆搿N覀兛梢詫⑨尫刨Y源的代碼核蘸,轉(zhuǎn)移到 onCleared()巍糯,這樣我們就不需要在 ActivityonCreate() 中添加觀察,onDestroy() 中移除觀察客扎。就和我們無(wú)需關(guān)心 onCleared() 的調(diào)用時(shí)機(jī)一樣祟峦。

override fun onCleared() {
        stateObservable.clearObservers()
        super.onCleared()
    }

現(xiàn)在,讓我們回到 LoginActivity 中徙鱼,創(chuàng)建一個(gè)具有延遲屬性的 ViewModel宅楞,在 onCreate() 中為其分配值。

 private lateinit var viewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        viewModel = ViewModelProviders.of(this)
            .get(LoginViewModel::class.java)
    }

當(dāng) ViewModel 不需要通過(guò)構(gòu)造傳遞參數(shù)時(shí)袱吆,可以按照上述方法實(shí)現(xiàn)厌衙。但是當(dāng)我們需要 ViewModel 通過(guò)構(gòu)造傳遞參數(shù)時(shí),則必須聲明一個(gè)工廠類(lèi)绞绒。

class LoginViewModelFactory(private val loginInteractor: LoginInteractor) : ViewModelProvider.NewInstanceFactory() {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
        LoginViewModel(loginInteractor) as T
}

Activity 中通過(guò)以下方式獲取 ViewModel 實(shí)例

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        viewModel = ViewModelProviders.of(this, LoginViewModelFactory(LoginInteractor()))
            .get(LoginViewModel::class.java)
    }

用 LiveData 替換 Observable

LiveData 可以安全的替換我們的 Observable 類(lèi)迅箩,需要注意的一點(diǎn)是,LiveData 默認(rèn)情況是不可變的(您無(wú)法改變其值)处铛。

這很棒,因?yàn)槲覀兿M枪驳墓战遥奖?Observer 可以訂閱撤蟆。但我們不希望在其他地方被修改。

但是堂污,另一方面家肯,數(shù)據(jù)需要是可變的,不然我們?yōu)槭裁磿?huì)觀察它呢盟猖?因此讨衣,訣竅是使用一個(gè)私有的屬性,并提供一個(gè)公共的 getter式镐。

在 kotlin 中反镇,它將是一個(gè)私有的屬性,和一個(gè)公共的 get() 屬性娘汞。

private val _loginState: MutableLiveData<ScreenState<LoginState>> = MutableLiveData()
val loginState: LiveData<ScreenState<LoginState>>
    get() = _loginState

而且我們也不再需要 onCleared() 了歹茶,因?yàn)?LiveData 具有生命周期意識(shí),它將在正確的時(shí)間停止觀察你弦。

要觀察它惊豺,最簡(jiǎn)潔的方式如下:

viewModel.loginState.observe(::getLifecycle, ::updateUI)

如果你不明白 函數(shù)引用,請(qǐng)查看我之前關(guān)于 函數(shù)引用 的文章禽作。

updateUI() 需要 ScreenState 作為參數(shù)尸昧,以便它適合 LiveData 的返回值。我可以將它用作函數(shù)引用旷偿。

private fun updateUI(screenState: ScreenState<LoginState>?) {
    ...
}

MainViewModel 也不需要 onResume() 了烹俗,相反爆侣,我們可以重寫(xiě)屬性的 getter,并在 LiveData 第一次觀察時(shí)衷蜓,執(zhí)行請(qǐng)求累提。

private lateinit var _mainState: MutableLiveData<ScreenState<MainState>>
 
val mainState: LiveData<ScreenState<MainState>>
    get() {
        if (!::_mainState.isInitialized) {
            _mainState = MutableLiveData()
            _mainState.value = ScreenState.Loading
            findItemsInteractor.findItems(::onItemsLoaded)
        }
        return _mainState
    }

MainActivity 的代碼和之前的類(lèi)似。

viewModel.mainState.observe(::getLifecycle, ::updateUI)

注意

之前的代碼似乎有點(diǎn)復(fù)雜磁浇,主要是因?yàn)槭褂昧诵碌目蚣苷悖?dāng)您了解它是如何工作的,一切將變得非常簡(jiǎn)單置吓。

肯定有一些新的樣板代碼无虚,例如 ViewModelFactory 和 獲取 ViewModel,或防止外部人員使用 LiveData 所定義的兩個(gè)屬性衍锚。我通過(guò)使用 Kotlin 的一些特性簡(jiǎn)化了本文的一些內(nèi)容友题,可以使您的代碼更加簡(jiǎn)潔,為了簡(jiǎn)單起見(jiàn)戴质,我并不打算在這里添加它們度宦。

正如我在開(kāi)頭所說(shuō)的,您是否使用 MVVM 或者 MVP 完全取決于您自己告匠。如果您目前的架構(gòu)使用 Mvp 運(yùn)行良好戈抄,我認(rèn)為沒(méi)有重構(gòu)的沖動(dòng),但了解 MVVM 的工作原理很有意思后专。因?yàn)槟t早會(huì)需要它划鸽。

我認(rèn)為我們?nèi)栽谔剿鳎?Android 中使用 MVVM 和架構(gòu)組件最優(yōu)的解決方案戚哎,我相信我的方案并不完美裸诽。所以,請(qǐng)讓我聽(tīng)到您內(nèi)心不同的聲音型凳,我很樂(lè)意根據(jù)反饋更新文章丈冬。

您可以在 GitHub 查看完整的代碼示例,(請(qǐng) star 支持 )

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末啰脚,一起剝皮案震驚了整個(gè)濱河市殷蛇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌橄浓,老刑警劉巖粒梦,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異荸实,居然都是意外死亡匀们,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)准给,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)泄朴,“玉大人重抖,你說(shuō)我怎么就攤上這事∽婊遥” “怎么了钟沛?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)局扶。 經(jīng)常有香客問(wèn)我恨统,道長(zhǎng),這世上最難降的妖魔是什么三妈? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任畜埋,我火速辦了婚禮,結(jié)果婚禮上畴蒲,老公的妹妹穿的比我還像新娘悠鞍。我一直安慰自己,他們只是感情好模燥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布咖祭。 她就那樣靜靜地躺著,像睡著了一般蔫骂。 火紅的嫁衣襯著肌膚如雪心肪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天纠吴,我揣著相機(jī)與錄音,去河邊找鬼慧瘤。 笑死戴已,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锅减。 我是一名探鬼主播糖儡,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼怔匣!你這毒婦竟也來(lái)了握联?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤每瞒,失蹤者是張志新(化名)和其女友劉穎金闽,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體剿骨,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡代芜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浓利。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挤庇。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钞速,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嫡秕,到底是詐尸還是另有隱情渴语,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布昆咽,位于F島的核電站驾凶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏潮改。R本人自食惡果不足惜狭郑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望汇在。 院中可真熱鬧翰萨,春花似錦、人聲如沸糕殉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)阿蝶。三九已至雳锋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間羡洁,已是汗流浹背玷过。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留筑煮,地道東北人辛蚊。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像真仲,于是被迫代替她去往敵國(guó)和親袋马。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容