情境
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 的優勢。