MVVM的學習記錄和思考

Acclex發表於2019-05-01

為什麼學習MVVM

公司的專案,一直是以Activity為主體,類MVC模式的進行開發的,加入現在的公司一年以來,因為現在接手專案比較老,因此之前一直是在做專案新的開發加填以前的老坑。專案的主頁甚至還在用已經廢棄了很久的TabActivity,之前下定決心改把主頁修改成了Activity+多Fragment的模式,以前的介面,Activity的邏輯過於複雜,正好接著這次重構的機會也和同事瞭解學習一下MVVM,為之後的開發算是做一個自己的準備吧。

學習的過程

DataBinding

Google為了MVVM,提供了不少的?以及框架,現在Google主推的就是Jetpack了,MVVM的核心就是資料繫結,這個在Android裡,因為XML作為view的功能極其孱弱,Google在Jetpack裡提供了Databinding的元件,讓XML和ViewModel進行資料繫結,通過繫結,如果ViewModel的資料變化,UI即可出現對應的響應。

XML內使用DataBinding

    <variable
        name="viewmodel"
        type="com.acclex.ViewModel" />
    <TextView
        android:text="@{viewmodel.user.name}"
複製程式碼

LiveData

為了讓ViewModel去運算元據,方便Activity和XML觀察資料的改變,Google還提供了LiveData這個框架,它的本質是一個類似RxJava的實現觀察者模式的框架,大致使用方式如下

ViewModel內

    private val _user: MutableLiveData<User> 
    val user:LiveData<User>
        get() = _user

    // 更改資料
    private fun updateUser() {
        _user.value = User() 
    }
複製程式碼

Activity或者Fragment內

    viewModel.user.observe(this, Observer<User> { user ->
        user?.let {
            //todo do something
        }
    })
複製程式碼

很簡單有效就可以實現觀察者模式,讓view層和ViewModel層解耦,通過這種方式去處理資料的變動,和RxJava實現的功能是一致的,因此MVVM也可以使用RxJava實現一樣的功能。通過觀察者模式,可以讓view與資料解耦開來,Activity以及Fragment不需要再去處理任何與資料相關的事情。

LiveData還是有一些好處的,因為它是Google開發封裝的,它自帶了生命週期的管理,因為它observe的直接是LifecycleOwner這個物件,如果LifecycleOwner的物件被銷燬,LiveData則會自己去clean掉,個人認為和生命週期繫結,這是一個很棒的優點,更多的優點,Google的官方文件有詳細的介紹:developer.android.google.cn/topic/libra…

ViewModel

Jetpack內還提供了ViewModel讓開發者去使用,ViewModel其實就是對業務邏輯和業務資料的操作,在ViewModel裡,不會也不可以持有任何View的引用,定義了一個ViewModel後,我們通常在View層使用val viewModel = ViewModelProviders.of(this).get(ViewModel::class.java)這樣的程式碼去獲取ViewModel的例項,這個this可以是Activity或者Fragment,ViewModel被初始化後會一直保留在記憶體內,直到它所作用域也就是Fragment觸發detached或者Activity觸發finishes,它才會被回收。 如果我們需要在初始化ViewModel的時候傳入構造引數,那麼我們必須要寫一個繼承自ViewModelProvider.NewInstanceFactory的類,程式碼如下

class SampleViewModelFactory(
        private val model: Model
) : ViewModelProvider.NewInstanceFactory() {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>) =
            with(modelClass) {
                when {
                    isAssignableFrom(SampleViewModel::class.java) ->
                        SampleViewModel(model)
                    else ->
                        throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
                }
            } as T

}
複製程式碼

這些都是為了方便開發者使用MVVM模式,Google在Jetpack內提供給我們的一些元件,單獨來看,這些元件的使用方法,學習成本並不高,同時並沒有涉及到Model層單獨做一個元件封裝,因為Model可以說是最自由,也是定製最多的元件。之前在我寫MVVM的demo的時候,我並沒有單獨的寫出一個Model,甚至將獲取資料寫在了ViewMode裡,讓ViewModel去獲取解析資料,並處理資料,之後的繼續學習,特別是閱讀了Google的android-architecture的原始碼之後,之前的思路可以說是完全錯誤的,接下來我們就來談談關於MVVM內Model層的定義與使用

Model

不管是MVC,MVP,MVVM的設計模式內,均存在Model層,可見Model層是極其重要的。但是在Android開發內,Model層反而是可能存在感最薄弱的一層,因為現在獲取資料的程式碼,不管是聯網獲取,或者是讀取資料庫,或者是讀取本地的資料,程式碼已經精簡到短短几行就可以實現,很多開發的時候,不自覺的把這些方式寫在了Activity、Fragment內,又或者是寫在了ViewModel或者是Presenter內,之前在讀一個MVVM實現的時候,就直接將獲取資料寫在了ViewModel內。

那麼這樣寫,會導致什麼問題?如果是簡單的資料以及相對簡單的邏輯,它並不會造成太多的影響,可讀性也沒有收到很多的影響,但是如果需要進行單元測試的話,資料耦合在了邏輯裡,會對單元測試造成極大的影響。這是我對這個問題的看法。(ps:小弟技術菜,沒有想到別的一些問題,只能看出這一點影響可能較大的問題,有補充歡迎評論留言,謝謝!)

按照規範標準來看,Model層是負責資料儲存,資料處理,以及獲取資料。但是Google不像ViewModel、LiveData、DataBinding提供了現成的規範以及標準,因此我對Model層其實是有一些問題的

Model層如何構造,包含那些介面以及基本方法

這可以說是Model層最關鍵的問題了,因為這關係到Model層的實現。這點我覺得還是需要參照程式碼來說明的。

剛好在這次專案的新的開發任務,我部分模組才用了MVVM去實現,並且嘗試了一下自己進行Model的設計,因此直接上程式碼,說一下我的理解。

interface BaseModel {
    interface ModelDataCallBack<T> {
        /**
         * 成功的回撥函式
         * @param result 成功返回需要的型別
         */
        fun onSuccess(result: T)

        /**
         * 失敗的回撥函式
         * @param errorLog 失敗後傳遞回去的錯誤的資料
         */
        fun onFailure(errorLog: String)
    }
}
複製程式碼

一個很簡單的基礎的Model,介面ModelDataCallBack負責回撥結果給ViewModel處理結果,因為每個ViewModel、Model需要的資料不一樣,因此回傳的結果是由初始化傳進來的泛型決定的。失敗的話,在我設想裡,應該是返回一個解析的結果或者異常log,也許是彈出一個Toast或者是一些別的邏輯,因此返回失敗的結果定義成了返回一個String。 因此ViewModel、Model具體實現的程式碼大致如下

Model內
class SampleModel : BaseModel {
    fun getData(callBack: BaseModel.ModelDataCallBack<List<User>>){
        // 如果成功
        callBack.onSuccess(listOf(User("A",15)))
        // 失敗
        callBack.onFailure("資料獲取失敗")
    }
}
複製程式碼
ViewModel內
class SampleViewModel(private val model:SampleModel) : ViewModel {

    private val _list = MutableLiveData<List<User>>()
    val list: LiveData<List<User>>
        get() = _list
    
    private fun updateUser() {
        model.getData(object :BaseModel.ModelDataCallBack<List<User>>{
            override fun onSuccess(result: List<User>) {
                list.value = result
            }

            override fun onFailure(errorLog: String) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            }
        })
    }
}
複製程式碼

如上程式碼,可以達成ViewModel負責處理邏輯,而Model負責獲取資料,ViewModel內接到成功或者失敗的回撥,可以觸發LiveData的資料更新,View層可以通過觀察LiveData內的資料,做到UI的更新或者變換。 這是我設想的一個簡單的Model層,如果在開發中,一個Model內有許多的獲取資料的方法或者介面,那麼會產生大量的介面回撥,如果是用kotlin的話,可以使用高階函式直接傳入成功或者失敗的回撥,類似如下程式碼

    fun getData(success:(List<User>) -> Unit,
                fail:(String) ->Unit){
        success.invoke(listOf(User("A",15)))
        fail.invoke("資料獲取失敗")
    }
複製程式碼

在我的想法裡,大量的介面回撥基本是不可能避免的,如果有dalao有更好的方案,歡迎評論區提出,感謝!

上述程式碼,只是一個很簡單的Model設計,如果在開發中使用這樣的Model,會碰到的問題還有如下:

  • 單元測試如何實現
  • BaseModel這個介面是否有存在的意義,是否只需要一個ModelDataCallBack介面即可
  • 如果有資料快取需求,應該怎麼處理

這些問題都是這個簡單的Model會碰到的,單元測試坑比較深,之後有空再單獨寫。 這個model的並不存在公共的實現方法,那麼根本不需要一個單獨的BaseModel介面,BaseModel的意義並不存在。如果這個model需要快取,如果只是model記憶體儲一個資料,那麼這樣的邏輯必然會影響到單元測試。因此這只是我的一個簡單的想法,後續還要完善。

Google MVVM Sample

在自己的這些想法之後,我專門去學習了一下Google的Sample的原始碼。放上Google的Sample,這裡是連結:github.com/googlesampl…。 先引用一張來自朋友部落格的圖片,部落格連結:部落格連結

MVVM的學習記錄和思考
每個Model是一個Respository都是一個DataSource介面的例項,裡面可能包含一種或者多種資料,每個資料型別都實現了DataSource介面。在Google的Sample裡,也是根據這種模式去實現的。這是很理想化的Model設計,本地的資料,快取的資料,測試的資料,單獨區分,每個負責對應的職責,將程式碼解耦開來,是非常好的。 下面上程式碼

interface TasksDataSource {

    interface LoadTasksCallback {

        fun onTasksLoaded(tasks: List<Task>)

        fun onDataNotAvailable()
    }

    interface GetTaskCallback {

        fun onTaskLoaded(task: Task)

        fun onDataNotAvailable()
    }

    fun getTasks(callback: LoadTasksCallback)

    fun getTask(taskId: String, callback: GetTaskCallback)

    fun saveTask(task: Task)

    fun completeTask(task: Task)

    fun completeTask(taskId: String)

    fun activateTask(task: Task)

    fun activateTask(taskId: String)

    fun clearCompletedTasks()

    fun refreshTasks()

    fun deleteAllTasks()

    fun deleteTask(taskId: String)
}
複製程式碼

Repository都實現了TasksDataSource介面,並且包含有多個實現了TasksDataSource介面的資料,或是本地資料,或是快取資料。並且只有getTasks和getTask這兩個函式有回撥方法,作為回撥給ViewModel的資料介面。別的函式作為Model暴露給ViewModel去操作處理資料的函式。提高了通用性,可以讓一個Repository去同時完成對快取資料或者新資料的操作。

總結一下

使用MVVM後,確實對程式碼解耦產生了極好的效果,程式碼的可讀性也上升的很多。文章裡很多東西還是本人的一些想法, 以及碰到的一些問題並沒有找到好的解決方案,同時在開發中,使用DataBinding之後程式碼的debug麻煩程度上升有點多,Model層的設計難度是我覺得最難得一個點,Google Sample裡我覺得也有一些不太好的地方,之後我會再寫一篇討論一下Google的這個MVVM的Sample。

感謝各位的閱讀,如果有什麼想法,歡迎提出意見、批評。

相關文章