[譯] 使用 Architecture Components 開發 MVVM 應用:MVP 開發者的實踐指南

13kmsteady發表於2018-12-24

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

譯者說

最近在學習 MVVM 相關的知識,在最新一期的 KotlinWeekly 發現了這篇文章。作者通過循序漸進的方式,向我們闡述如何實現 MVVM,以及如何使用 Android Jetpack Components 元件來構建 MVVM 應用。讀完以後,收穫頗豐。為了讓更多的開發者瞭解到 MVVM,我斗膽翻譯過來,這便是這篇文章的來由。英語渣渣,如有錯誤,還請指正。

正文

導語

自從 Google 正式釋出了 Android Jetpack Components 架構元件,MVVM 已然成為了 Android Apps 官宣的主流開發模式。我認為是時候,提供一些行之有效的幫助,幫助使用 Mvp 模式的開發者來理解 MVVM 模式。

如果您碰巧看到這篇部落格,但是不知道怎麼在 Android 中使用 Mvp 模式,推薦您檢視我之前寫的關於 Mvp 的部落格。

MVVM vs Mvp - 我需要去重構我的 App 嗎?

在相當長的一段時間內,Mvp 似乎是用來 降低 UI 渲染業務邏輯 之間耦合的最受歡迎的開發模式。但是,現在我們有了新的選擇。

許多開發者詢問我,是否應該逃避 Mvp,或者當開始新的專案如何設計架構。下面是一些想法:

  • Mvp 沒有消失。它仍然是完全有效的開發模式,如果您之前使用它,也可以接著使用。
  • MVVM 作為新的開發模式,不一定更好。但谷歌所做的具體實施是很有道理的,之前使用 MVP 的原因是:它與 Android 框架非常吻合,並且上手難度不大。
  • 使用 Mvp 並不意味著,你不可以使用 Android Jetpack Components 架構元件。可能 ViewModel 沒有多大的作用(它是 Presenter 的替代者),但是其他元件可以在專案中使用。
  • 您不需要立即重構您的 App,如果您對 Mvp 非常滿意,請繼續享受它。一般來說,最好保持一個安全,可靠的架構。而不是在專案中使用新的技術棧,畢竟重構是需要成本的。

MVVM 和 MVp 的差異

幸運的是,如果您之前熟悉 Mvp,學習 MVVM 將非常容易!在 Android 開發中,兩者只有一點點的差異:

在 Mvp 中,PresenterView 通過 介面 聯絡。 在 MVVM 中,ViewModelView 通過 觀察者模式 通訊。

我知道,如果你曾閱讀過維基百科關於 MVVM 的定義。將會發現和我之前所說的完全不符。但是在 Android 開發領域中,拋開 Databinding 不談,在我看來,這將是理解 MVVM 的最佳方式。

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

我將使用 MVVM 來改造之前的 androidmvp 例子,MVVM 示例程式碼請戳這裡 androidmvvm

我暫時不使用 Architecture Components,先自己實現。之後我們就可以清晰的認識到 Google 新推出的 Android Jetpack Components 是如何工作的,以及如何讓開發變得更加高效。

建立一個 Observable 類

當我們使用 Observable 模式時,需要一個可以觀察的類。該類將持有 Observer 和將傳送給 Observer 的泛型型別的值, 以及當值發生改變,通知到 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 來表示 UI 更改

由於我們現在無法直接與 View 進行通訊,View 也不知道該怎麼顯示。我發現一個靈活的方式,通過一個 Model 類來表示 UI 狀態。

舉個例子,如果我們希望介面顯示一個進度條,我們將傳送一個 Loading 狀態,消費該狀態的方式完全由檢視決定。

對於這種特殊情況,我建立了一個 ScreenState 類,它接受一個表示檢視所需狀態的泛型型別。

每個介面都有一些共同的狀態,例如 LoadingErroor。然後是每個介面顯示的具體狀態。

可以使用以下密閉類,來表示通用的 ScreenState

sealed class ScreenState<out T>{
    object Loading:ScreenState<Nothing>()
    class Render<T>(val renderState:T):ScreenState<T>()
}
複製程式碼

對於特定狀態,我們可能需要額外的定義。對於登陸狀態,列舉類就足夠了。

enum class LoginState{
    Success,
    WrongUserName,
    WrongUserPassword
}
複製程式碼

但是對於 MainState,我們正在顯示列表和訊息,列舉類無法提供足夠的支援,所以密閉類再次獲得我的青睞(稍後會看到具體原因)。

sealed class MainState{
    class ShowItems(val items:List<String>):MainState()
    class showMessage(val items:String):MainState()
}
複製程式碼

將 Presenter 轉換為 ViewModel

我們不再需要定義 View 介面,你可以擺脫它。因為我們將使用 Observable 替代。

如下示例:

val stateObservable = Observable<ScreenState<LoginState>>()
複製程式碼

之後,當我們想顯示進度條表示載入狀態時,只需要呼叫 LoadingStateObserver

fun validateCredentials(username: String, password: String) {
    stateObservable.callObservers(ScreenState.Loading)
    loginInteractor.login(username, password, this)
}
複製程式碼

當登入完成時,需要展示成功資訊:

override fun onSuccess() {
 stateObservable.callObservers(ScreenState.Render(LoginState.Success))
}
複製程式碼

老實說,登入成功的狀態可以用不同的方式實現,如果我們想要更明確,可以使用 LoginState.NavigateToMain 或者類似的方式進入首頁。

但這取決於更多因素,取決於應用程式架構。我會這樣做。

然後,在 ViewModelonDestroy() 中,我們清除了 Observers,避免潛在的記憶體洩漏問題。

在 Activity 中使用 ViewModel

目前 Activity 還無法充當 ViewModel 中 View 的角色,因此 觀察者模式 將會受到重用。

首先,初始化 ViewModel

private val viewModel = LoginViewModel(LoginInteractor())
複製程式碼

之後,在 onCreate() 中觀察狀態,當狀態發生變化,將會呼叫 updateUI()

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

在這裡,感謝密閉類和列舉類。通過使用 when 表示式,一些變得如此簡單。我分兩步處理狀態:首先是一般狀態,然後是特定的 LoginState

第一個 when 表示式分支:顯示載入狀態的進度條。如果是其它特定狀態,需要呼叫另外的函式處理。

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

第二個 when 表示式分支:首先隱藏進度條(如果可見),如果是成功狀態,則進入首頁。如果是錯誤狀態,則提示相應的錯誤資訊

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)
        }
    }
複製程式碼

當點選登入按鈕,呼叫 ViewModel 中的 onLoginClicked() 進行操作。

 private fun login() {
        viewModel.onLoginClicked(username.text.toString(), password.text.toString())
    }
複製程式碼

然後,在 Activity 中的 onDestroy() 呼叫 ViewModelonDestroy() 釋放資源(這樣就可以分離觀察者)。

override fun onDestroy() {
        viewModel.onDestroy()
        super.onDestroy()
    }
複製程式碼

使用 Architecture Components 修改程式碼

通過之前自己實現 MVVM 的 ViewModel,以便您可以輕鬆的看到差異。到目前為止,與 MVP 相比,MVVM 並沒有帶來更多的好處。

但也要一些不同,最重要的一點是您可以忘記 Activity 的銷燬,所以您可以脫離它的生命週期,隨時做你的工作。特別感謝 ViewModelLiveData。當 Activity 重新建立或者被銷燬時,您無需擔心應用的崩潰。

這是工作原理:當 Activity 被重新建立,ViewModel 仍然存在,當 Activity 被永久殺死的時候,將會呼叫 ViewModelonCleared()

viewmodel-lifecycle.png

由於 LiveData 也具有生命週期意識,因此它知道何時跟 LifecycleOwner 建立和斷開聯絡。所以您無需關心它。

我並不打算深入講解 Architecture Components 的工作原理(因為在官方的開發者指南中有更深刻的解釋),所以讓我們繼續探索實現 MVVM

在專案中使用 Architecture Components,需要新增以下依賴

    implementation "android.arch.lifecycle:extensions:1.1.1"
複製程式碼

如果您使用其他元件,如:Room 。或者在 AndroidX 上使用這些元件,更多內容請參考 這裡

Architecture Components ViewModel

使用 ViewModel 非常簡單,你只需要繼承 ViewModel 即可。

class LoginViewModel(private val loginInteractor: LoginInteractor) : ViewModel()
複製程式碼

刪除 onDestroy(),因為它不再需要了。我們可以將釋放資源的程式碼,轉移到 onCleared(),這樣我們就不需要在 ActivityonCreate() 中新增觀察,onDestroy() 中移除觀察。就和我們無需關心 onCleared() 的呼叫時機一樣。

override fun onCleared() {
        stateObservable.clearObservers()
        super.onCleared()
    }
複製程式碼

現在,讓我們回到 LoginActivity 中,建立一個具有延遲屬性的 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)
    }
複製程式碼

ViewModel 不需要通過構造傳遞引數時,可以按照上述方法實現。但是當我們需要 ViewModel 通過構造傳遞引數時,則必須宣告一個工廠類。

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

    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
        LoginViewModel(loginInteractor) as T
}
複製程式碼

Activity 中通過以下方式獲取 ViewModel 例項

    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 類,需要注意的一點是,LiveData 預設情況是不可變的(您無法改變其值)。

這很棒,因為我們希望它是公共的,方便 Observer 可以訂閱。但我們不希望在其他地方被修改。

但是,另一方面,資料需要是可變的,不然我們為什麼會觀察它呢?因此,訣竅是使用一個私有的屬性,並提供一個公共的 getter

在 kotlin 中,它將是一個私有的屬性,和一個公共的 get() 屬性

private val _loginState: MutableLiveData<ScreenState<LoginState>> = MutableLiveData()
複製程式碼
val loginState: LiveData<ScreenState<LoginState>>
    get() = _loginState
複製程式碼

而且我們也不再需要 onCleared() 了,因為 LiveData 具有生命週期意識,它將在正確的時間停止觀察。

要觀察它,最簡潔的方式如下:

viewModel.loginState.observe(::getLifecycle, ::updateUI)
複製程式碼

如果你不明白 函式引用,請檢視我之前關於 函式引用 的文章。

updateUI() 需要 ScreenState 作為引數,以便它適合 LiveData 的返回值。我可以將它用作函式引用。

private fun updateUI(screenState: ScreenState<LoginState>?) {
    ...
}
複製程式碼

MainViewModel 也不需要 onResume() 了,相反,我們可以重寫屬性的 getter,並在 LiveData 第一次觀察時,執行請求。

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 的程式碼和之前的類似。

viewModel.mainState.observe(::getLifecycle, ::updateUI)
複製程式碼

注意

之前的程式碼似乎有點複雜,主要是因為使用了新的框架,當您瞭解它是如何工作的,一切將變得非常簡單。

肯定有一些新的樣板程式碼,例如 ViewModelFactory 和 獲取 ViewModel,或防止外部人員使用 LiveData 所定義的兩個屬性。我通過使用 Kotlin 的一些特性簡化了本文的一些內容,可以使您的程式碼更加簡潔,為了簡單起見,我並不打算在這裡新增它們。

正如我在開頭所說的,您是否使用 MVVM 或者 MVP 完全取決於您自己。如果您目前的架構使用 Mvp 執行良好,我認為沒有重構的衝動,但瞭解 MVVM 的工作原理很有意思。因為您遲早會需要它。

我認為我們仍在探索,在 Android 中使用 MVVM 和架構元件最優的解決方案,我相信我的方案並不完美。所以,請讓我聽到您內心不同的聲音,我很樂意根據反饋更新文章。

您可以在 GitHub 檢視完整的程式碼示例,(請 star 支援 )

相關文章