如何使用Android MVVM架構(一)-使用ViewModel、LiveData、Factory以及Repository

如何使用Android MVVM架構(一)-使用ViewModel、LiveData、Factory以及Repository

情境

MVVM 架構是很早就提出來的一種概念,2017年 Google 官方提供相關 Framework 來支援這個架構,它可以讓開發者能夠專注在邏輯層面,讓程式更好維護、測試的一種架構。

完整程式碼

如果需要完整的程式碼,可以到 GitHub 上觀看或者下載。

程式碼說明

如果你要建立一個 Android MVVM 架構的 App,就得操作 Android 新推出的 ViewModel,一開始如果要使用 ViewModel,要先記得在 Gradle 上面 implements 相對應的 Library 。

implementation 'android.arch.lifecycle:extensions:1.1.1'  
annotationProcessor "android.arch.lifecycle:compiler:1.1.1"

如果你是 androidx 系列的則是使用以下 Library。

implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'  
annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.0.0'

在使用 ViewModel 前要先建立四個類別的觀念:

  • ViewModelProvider.Factory
  • ViewModel
  • Repository
  • LiveData

Factory 用來生成 ViewModel 實體物件。
ViewModel 用來跟 Repository 溝通取得資料。
Repository 是從來源(Source)取得資料加以處理,透過這樣的分工合作就產生了以下示意圖的模式。


首先 Factory 就是透過工廠模式來處理,以下解法不是最好的,等到後續使用 Koin 就可以用比較動態的方式來生成我們的 ViewModel 了,程式碼如下。

class InfoFactory(var infoRepository: InfoRepository) : ViewModelProvider.Factory {  
  
    override fun <T : ViewModel> create(@NonNull modelClass: Class<T>): T {  
        if (modelClass.isAssignableFrom(InfoViewModel::class._java_)) {  
            return InfoViewModel(infoRepository) as T  
        }  
        throw IllegalArgumentException("Unknown ViewModel class")  
    }  
}

首先我們必須先建立好 Factory,它是 Factory 的子類別用來外部生成 ViewModel 的一個工具類別,稍後我們可以透過它來產生一個 ViewModel 的實體物件。

此時,我們也同時建立好一個類別叫做 ApiInfoViewModel,它是繼承 ViewModel 的子類別。

class InfoViewModel(private var infoRepository: InfoRepository): ViewModel() {  
    private var userInfoLiveData = MutableLiveData<UserData>()  
    fun callInfo():LiveData<UserData>{  
        val data = infoRepository.loadInfo()  
        userInfoLiveData._value_ = data  
        return userInfoLiveData  
    }  
}

透過 ViewModel 可以將我們的 Repository 實體傳入以後,透過 Repository 的方法來取得我們所需的資料,這邊可以看到我們宣告了一個 LiveData 物件,目的是拿來接收從 Repository 得到的資料,最後可以讓 View 來進行存取。

先來解釋一下什麼是 LiveData。

LiveData 是 Google 開發出來的幫我們處理生命週期時資料存活的工具,也就是說你跟 LiveData 講要跟著誰的(Owner 是哪個 Activity 或 Fragment)生命週期,那麼它就會隨著該 Owner 活著或消滅。

所以後續我們會在 Activity 上面看到以下程式碼。

infoViewModel.callInfo().observe(this, Observer {
    Toast.makeText(this, "user name:${it.userName} user age:${it.userAge}", Toast.LENGTH_SHORT).show()
})

observe 的第一個參數 this,就是跟 callInfo() 回傳的 LiveData 說跟著當前 Activity 的生命週期。

透過 observe 可以在非同步的資料取得/處理完成以後,把結果回傳回來,這種就是透過觀察者模式來完成我們的 Callback 處理。

那我們來看一下非同步資料到底在做些什麼?先看一下 Repository 類別。

class InfoRepository {  
    fun loadInfo(): UserData {  
        val userData = UserData()  
        userData.userName = "jake"  
        userData.userAge = 30  
        sleep(3000)  
        return userData  
    }  
}

這邊假裝非同步的情況回來資訊,所以裝好資料以後先讓它睡個三秒,這樣一來,我們就可以透過 Repository 拿到對應的資料了,這邊還有一些問題,譬如說如果要假設非同步其實應該要多開一個 Thread 讓他在後面執行任務,等到拿到資料以後,再透過 Callback 的方式來拿到我們的 UserData,基於這樣的理由,我們將 Repository 轉換成這樣的模式。

class InfoRepository {  
    fun loadInfo(task: OnTaskFinish) {  
        Executors.newSingleThreadExecutor().submit {  
            val userData = UserData()  
            userData.userName = "jake"  
            userData.userAge = 30  
            Thread.sleep(3000)  
            task.onFinish(userData)  
        }  
    }  
}  
  
interface OnTaskFinish {  
    fun onFinish(data: UserData)  
}

所以 ViewModel 部分也需要調整一下。

infoRepository.loadInfo(object : OnTaskFinish {  
    override fun onFinish(data: UserData) {  
        userInfoLiveData.postValue(data)  
    }  
})  
return userInfoLiveData

讓我們鏡頭回來 Activity,看看怎麼透過 ViewModel 取到非同步資料,首先宣告三個變數。

private lateinit var infoViewModel: InfoViewModel  
private lateinit var infoFactory: InfoFactory  
private lateinit var infoRepository: InfoRepository

在 Activity 內初始化這三個變數。

infoRepository = InfoRepository()  
infoFactory = InfoFactory(infoRepository)  
infoViewModel = ViewModelProviders.of(this, infoFactory).get(InfoViewModel::class.java)

這邊要注意的是 ViewModelProviders 後面有個 s,如果你用到ViewModelProvider 是找不到 of 這個方法的。

這樣一來我們就可以透過前面講的 observe 這個方法來取得所需的資料進行畫面的操作。

get_info.setOnClickListener {
    val dialog = ProgressDialog.show(
        this, "",
        "Loading. Please wait...", true
    )
    dialog.show()
    infoViewModel.callInfo().observe(this, Observer {
        dialog.dismiss()
        Toast.makeText(this, "user name:${it.userName} user age:${it.userAge}", Toast.LENGTH_SHORT).show()
    })
}

上面程式碼透過 observe 的方法,來取得非同步的資料,當資料回來之前先用簡易的等待視窗顯示,等到資料回來了以後,再將讀取視窗關閉,效果就會如下。


其實 Repository 是一種概念,它不單單只是取得單一資料而已,其實他還可以透過各種來源(API、檔案、資料庫等…)來統整資料的概念,如下圖。


不只這樣,它還可以搭配 Retrofit、Room 以及 RxJava 來處理任務,這邊後續會再把相關教學補上。

這樣就是我們的簡易 MVVM 架構,後續還可以做很多事情,加入各種好用的第三方,來強化我們導入MVVM 的優勢。