ViewModel 甫一發(fā)布炭菌,便成為了 Jetpack 中的核心組件之一醇王。我們在 2019 年做的一份開發(fā)者問卷顯示敌卓,超過 40% 的 Android 開發(fā)者已經(jīng)在自己的應(yīng)用中使用了 ViewModel遂黍。ViewModel 可以將數(shù)據(jù)層與 UI 分離俐填,而這種架構(gòu)不僅可以簡化 UI 的生命周期的控制安接,也能讓代碼獲得更好的可測試性。如果想了解更多英融,可以參考 ViewModel: 簡單介紹視頻和官方文檔盏檐。
由于 ViewModel 是許多功能實(shí)現(xiàn)的基礎(chǔ),我們在過去的幾年里做了許多工作來改進(jìn) ViewModel 的易用性驶悟,也讓它能夠更加簡便地與其他組件庫相結(jié)合胡野。下面的文章中,我將介紹 ViewModel 的四種集成方式:
- ViewModel 中的 Saved State —— 后臺進(jìn)程重啟時痕鳍,ViewModel 的數(shù)據(jù)恢復(fù)硫豆;
- 在 NavGraph 中使用 ViewModel —— ViewModel 與導(dǎo)航 (Navigation) 組件庫的集成龙巨;
- ViewModel 配合數(shù)據(jù)綁定(data-binding) —— 通過使用 ViewModel 和 LiveData 簡化數(shù)據(jù)綁定;
- viewModelScope —— Kotlin 協(xié)程與 ViewModel 的集成熊响。
ViewModel 的 Saved State —— 后臺進(jìn)程重啟時旨别,ViewModel 的數(shù)據(jù)恢復(fù)
- 于 lifecycle-viewmodel-savedstate 的 1.0.0-alpha01 版本時加入
- 支持 Java 和 Kotlin
onSaveInstanceState 帶來的挑戰(zhàn)
ViewModel 一發(fā)布,執(zhí)行 onSaveInstanceState 的相關(guān)的邏輯時要如何操作 ViewModel汗茄,便成為了一個令人困惑的問題秸弛。Activity 和 Fragment 通常會在下面三種情況下被銷毀:
- 從當(dāng)前界面永久離開: 用戶導(dǎo)航至其他界面或直接關(guān)閉 Activity (通過點(diǎn)擊返回按鈕或執(zhí)行的操作調(diào)用了 finish() 方法)。對應(yīng) Activity 實(shí)例被永久關(guān)閉洪碳;
- Activity 配置 (configuration) 被改變: 例如递览,旋轉(zhuǎn)屏幕等操作,會使 Activity 需要立即重建瞳腌;
- 應(yīng)用在后臺時绞铃,其進(jìn)程被系統(tǒng)殺死: 這種情況發(fā)生在設(shè)備剩余運(yùn)行內(nèi)存不足,系統(tǒng)又亟須釋放一些內(nèi)存的時候纯趋。當(dāng)進(jìn)程在后臺被殺死后憎兽,用戶又返回該應(yīng)用時,Activity 也需要被重建吵冒。
在后兩種情況中纯命,我們通常都希望重建 Activity。ViewModel 會幫您處理第二種情況痹栖,因?yàn)樵谶@種情況下 ViewModel 沒有被銷毀亿汞;而在第三種情況下, ViewModel 被銷毀了揪阿。所以一旦出現(xiàn)了第三種情況疗我,便需要在 Activity 的 onSaveInstanceState 相關(guān)回調(diào)中保存和恢復(fù) ViewModel 中的數(shù)據(jù)。我在 ViewModels: 持久化南捂、onSaveInstanceState()吴裤、恢復(fù) UI 狀態(tài)與加載器一文中更加詳細(xì)地描述了這兩種情況的區(qū)別。
Saved State 模塊
現(xiàn)在溺健,ViewModel Saved State 模塊將會幫您在應(yīng)用進(jìn)程被殺死時恢復(fù) ViewModel 的數(shù)據(jù)麦牺。在免除了與 Activity 繁瑣的數(shù)據(jù)交換后,ViewModel 也真正意義上的做到了管理和持有所有自己的數(shù)據(jù)鞭缭。
ViewModel 的這一新功能是通過 SavedStateHandle 實(shí)現(xiàn)的剖膳。SavedStateHandle 和 Bundle 一樣,以鍵值對形式存儲數(shù)據(jù)岭辣,它包含在 ViewModel 中吱晒,并且可以在應(yīng)用處于后臺時進(jìn)程被殺死的情況下幸存下來。諸如用戶 id 等需要在 onSaveInstanceState 時得到保存下來的數(shù)據(jù)沦童,現(xiàn)在都可以存在 SavedStateHandle 中仑濒。
設(shè)置 Save State 模塊
現(xiàn)在讓我們看看如何使用 SaveState 組件叹话。注意接下來的代碼會和 Lifecycles Codelab 第六步中的一段代碼十分相似。那段是 Java 代碼躏精,而接下來的是 Kotlin 代碼:
第一步: 添加依賴
SaveStateHandle 目前在一個獨(dú)立的模塊中渣刷,您需要在依賴中添加:
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedsta
注意,本文發(fā)布時 lifecycle 組件的最新穩(wěn)定版為 2.2.0矗烛,如果您希望持續(xù)關(guān)注相關(guān)組件庫的進(jìn)展辅柴,可以查看 lifecycle 版本發(fā)布文檔。
第二步: 修改調(diào)用 ViewModelProvider 的方式
接下來瞭吃,您需要創(chuàng)建一個持有 SaveStateHandle 的 ViewModel碌嘀。在 Activity 或 Fragment 的 onCreate 方法中,將 ViewModelProvider 的調(diào)用修改為:
//下面的 Kotlin 擴(kuò)展需要依賴以下或更新新版本的 ktx 庫:
//androidx.fragment:fragment-ktx:1.0.0(最新版本 1.2.4) 或
//androidx.activity:activity-ktx:1.0.0 (最新版本 1.1.0)
val viewModel by viewModels { SavedStateViewModelFactory(application, this) }
// 或者不使用 ktx
val viewModel = ViewModelProvider(this, SavedStateViewModelFactory(application, this))
.get(MyViewModel::class.java)
創(chuàng)建 ViewModel 的類是 ViewModel 工廠 (ViewModel factory)歪架,而創(chuàng)建包含 SaveStateHandle 的 View Model 的工廠類是 SavedStateViewModelFactory股冗。通過此工廠創(chuàng)建的 ViewModel 將持有一個基于傳入 Activity 或 Fragment 的 SaveStateHandle。
第三步: 使用 SaveStateHandle
當(dāng)前面的步驟準(zhǔn)備完成時和蚪,您就可以在 ViewModel 中使用 SavedStateHandle 了止状。下面是一個保存用戶 ID 的示例:
class MyViewModel(state :SavedStateHandle) :ViewModel() {
// 將Key聲明為常量
companion object {
private val USER_KEY = "userId"
}
private val savedStateHandle = state
fun saveCurrentUser(userId: String) {
// 存儲 userId 對應(yīng)的數(shù)據(jù)
savedStateHandle.set(USER_KEY, userId)
}
fun getCurrentUser(): String {
// 從 saveStateHandle 中取出當(dāng)前 userId
return savedStateHandle.get(USER_KEY)?: ""
}
}
- 構(gòu)造方法: SavedStateHandle 作為構(gòu)造方法參數(shù)傳入 MyViewModel;
- 保存: saveNewUser 方法展示了使用鍵值對的形式保存 USER_KEY 和 userId 到 SaveStateHandle 的例子攒霹。每當(dāng)數(shù)據(jù)更新時怯疤,要保存新的數(shù)據(jù)到 SavedStateHandle;
- 獲取: 如代碼中所示催束,調(diào)用 savedStateHandle.get(USER_KEY) 方法獲取被保存的 userId集峦。
現(xiàn)在,無論是第二還是第三種情況下抠刺,SavedStateHandle 都可以幫您恢復(fù)界面數(shù)據(jù)了塔淤。
如果您想要在 ViewModel 中使用 LiveData,可以調(diào)用 SavedStateHandle.getLiveData()速妖,示例如下:
// getLiveData 方法會取得一個與 key 相關(guān)聯(lián)的 MutableLiveData
// 當(dāng)與 key 相對應(yīng)的 value 改變時 MutableLiveData 也會更新高蜂。
private val _userId : MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY)
// 只暴露一個不可變 LiveData 的情況
val userId : LiveData<String> = _userId
如需了解更多,請移步至 Lifecycles Codelab 第六步和官方文檔罕容。
ViewModel 與 Jetpack 導(dǎo)航: 在 NavGraph 中使用 ViewModel
- 于 navigation 的 2.1.0-rc01 版本時加入
- 支持 Java 與 Kotlin
共享 ViewModel 數(shù)據(jù)所帶來的挑戰(zhàn)
Jetpack 導(dǎo)航組件 (Navigation) 十分適用于那些只有少量或一個 Activity妨马,但是 Activity 中會包含多個 Fragment 的應(yīng)用。Ian Lake 在他的演講: 單 Activity 架構(gòu): 為什么杀赢、什么情況下以及如何使用中介紹了一些我們選擇單一 Activity 架構(gòu)的原因,而與本文相關(guān)的一點(diǎn)湘纵,是這種架構(gòu)允許在多個界面 (destination) 間共享 ActivityViewModel脂崔。您可以用 Activity 創(chuàng)建一個 ViewModel 實(shí)例,然后從這個 Activity 中的任一個 Fragment 中獲得 ViewModel 的引用:
// 在Fragment的 onCreate 或 onActivityCreated 方法中執(zhí)行
// 這個Kotlin擴(kuò)展需要依賴最KTX庫:androidx.fragment:fragment-ktx:1.1.0
val sharedViewModel: ActivityViewModel by activityViewModels()
假設(shè)我們有這樣一個單 Activity 應(yīng)用梧喷,它包含了八個 Fragment砌左,其中四個 Fragment 是購買支付流程:這四個頁面需要共享一些諸如收貨地址脖咐、是否使用了優(yōu)惠券等信息。按照前面所講的做法汇歹,需要共享的數(shù)據(jù)會放在一個 ActivityViewModel 中屁擅,但這同時也意味著所有八個頁面都會共享這些數(shù)據(jù)。支付流程外的界面并不需要關(guān)心這些數(shù)據(jù)产弹,這么做顯然并不合適派歌。
ViewModel 與 NavGraph 集成
Navigation 2.1.0 中引入了依托一個導(dǎo)航圖 (navigation graph) 創(chuàng)建 ViewModel 的功能。在使用時痰哨,您需要先把一個界面集合 (例如: 登錄流程胶果、支付流程的相關(guān)界面),放到一個嵌套導(dǎo)航圖 (nested navigation graph) 中斤斧。此時再通過嵌套導(dǎo)航圖創(chuàng)建出 ViewModel早抠,便可以在相關(guān)界面中共享數(shù)據(jù)了。
想要創(chuàng)建嵌套導(dǎo)航圖撬讽,您需要選中對應(yīng)流程相關(guān)的界面蕊连,點(diǎn)擊鼠標(biāo)右鍵,并選擇 Nested Graph → New Graph:
注意嵌套導(dǎo)航圖在 XML 文件中的 id游昼,在這里是 checkout_graph:
<navigation app:startDestination="@id/homeFragment" ...>
<fragment android:id="@+id/homeFragment" .../>
<fragment android:id="@+id/productListFragment" .../>
<fragment android:id="@+id/productFragment" .../>
<fragment android:id="@+id/bargainFragment" .../>
<navigation
android:id="@+id/checkout_graph"
app:startDestination="@id/cartFragment">
<fragment android:id="@+id/orderSummaryFragment".../>
<fragment android:id="@+id/addressFragment" .../>
<fragment android:id="@+id/paymentFragment" .../>
<fragment android:id="@+id/cartFragment" .../>
</navigation>
</navigation>
以上工作完成時甘苍,便可以使用 by navGraphViewModels 獲取到對應(yīng)的 ViewModel:
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
Java 中同樣適用,代碼如下:
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 設(shè)置其他 fragment
NavController navController = NavHostFragment.findNavController(this);
ViewModelProvider viewModelProvider = new ViewModelProvider(this,
navController.getViewModelStore(R.id.checkout_graph));
CheckoutViewModel viewModel = viewModelProvider.get(CheckoutViewModel.class);
// 使用 Checkout ViewModel
}
需要注意的是酱床,嵌套導(dǎo)航圖相對于導(dǎo)航圖的其他部分是一個獨(dú)立的整體羊赵。您無法導(dǎo)航至嵌套導(dǎo)航圖中包含的某個特定界面;當(dāng)您導(dǎo)航至一個嵌套導(dǎo)航圖時扇谣,打開的只會是其中的開始界面 (startDestination)昧捷。這種特性使得嵌套導(dǎo)航圖適合用于封裝特定流程的界面組合,比如前面提到過的登錄和支付流程罐寨。
ViewModel 與 NavGraph 的集成靡挥,是 2019 年 I/O 大會所發(fā)布的關(guān)于 Navigation 框架的新特性之一。
詳細(xì)了解更多鸯绿,請參閱:
-
主題演講: Jetpack Navigation 的主題演講
https://v.youku.com/v_show/id_XNDE3NzAzNzQ4NA==.html -
官方文檔: 以編程方式與導(dǎo)航組件交互
https://developer.android.google.cn/guide/navigation/navigation-programmatic#share_
ViewModel 與 Data Binding: 在 Data Binding 中使用 ViewModel 和 LiveData
- 于 Android Studio 的 3.1 版本時加入
- 支持 Java 與 Kotlin
移除 LiveData 相關(guān)的模板代碼
ViewModel跋破、LiveData 與 Data Binding 的集成方式并不是什么新功能,但它始終非常好用瓶蝴。ViewModel 通常都包含一些 LiveData毒返,而 LiveData 意味著可以被監(jiān)聽。所以最常見的使用場景是在 Fragment 中給 LiveData 添加一個觀察者:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
myViewModel.name.observe(this, { newName ->
// 更新UI舷手,這里是一個TextView
nameTextView.text = newName
})
}
Data Binding 是一個通過觀察數(shù)據(jù)變化來更新 UI 的組件庫拧簸。通過 ViewModel、LiveData 和 Data Binding 的組合男窟,您可以移除以往給 LiveData 添加觀察者的做法盆赤,改為直接在 XML 中綁定 View Model 和 LiveData贾富。
使用 Data Binding、ViewModel 和 LiveData
假設(shè)您希望在 XML 布局文件中引用 ViewModel:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewmodel"
type="com.android.MyViewModel"/>
</data>
<... Rest of your layout ...>
</layout>
調(diào)用 binding.setLifecycleOwner(this) 方法牺六,然后將 ViewModel 傳遞給 binding 對象颤枪,就可以將 LiveData 與 Data Binding 結(jié)合起來:
class MainActivity : AppCompatActivity() {
// 這個ktx擴(kuò)展需要依賴 androidx.activity:activity-ktx:1.0.0
// 或更新版本
private val myViewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//填充視圖并創(chuàng)建 Data Binding 對象
val binding: MainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.main_activity)
//聲明這個 Activity 為 Data Binding 的 lifecycleOwner
binding.lifecycleOwner = this
// 將 ViewModel 傳遞給 binding
binding.viewmodel = myViewModel
}
}
現(xiàn)在,您可以像下面這樣使用 ViewModel:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewmodel"
type="com.android.MyViewModel"/>
</data>
<TextView
android:id="@+id/name"
android:text="@{viewmodel.name}"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
</layout>
注意淑际,這里的 viewmodel.name 既可以是 String 類型畏纲,也可以是 LiveData。如果它是 LiveData庸追,那么 UI 將根據(jù) LiveData 值的改變自動刷新霍骄。
ViewMode 與 Kotlin 協(xié)程: viewModelScope
- 于 Lifecycle 的 2.1.0 版本時加入
- 只支持 Kotlin
Android 平臺上的協(xié)程
通常情況下,我們使用回調(diào) (Callback) 處理異步調(diào)用淡溯,這種方式在邏輯比較復(fù)雜時读整,會導(dǎo)致回調(diào)層層嵌套,代碼也變得難以理解咱娶。Kotlin 協(xié)程 (Coroutines) 同樣適用于處理異步調(diào)用米间,它讓邏輯變得簡單的同時,也確保了操作不會阻塞主線程膘侮。如果您不了解協(xié)程屈糊,這里有一系列很棒的博客《在 Android 開發(fā)中使用協(xié)程》以及 codelab: 在 Android 應(yīng)用中使用 Kotlin 協(xié)程以供參考。
一段簡單的協(xié)程代碼:
// 下面是示例代碼琼了,真實(shí)情景下不要使用 GlobalScope
GlobalScope.launch {
longRunningFunction()
anotherLongRunningFunction()
}
這段示例代碼只啟動了一個協(xié)程逻锐,但我們在真實(shí)的使用環(huán)境下很容易創(chuàng)建出許多協(xié)程,這就難免會導(dǎo)致有些協(xié)程的狀態(tài)無法被跟蹤雕薪。如果這些協(xié)程中剛好有您想要停止的任務(wù)時昧诱,就會導(dǎo)致任務(wù)泄漏 (work leak)。
為了防止任務(wù)泄漏所袁,您需要將協(xié)程加入到一個 CoroutineScope 中盏档。CoroutineScope 可以持續(xù)跟蹤協(xié)程的執(zhí)行,它可以被取消燥爷。當(dāng) CoroutineScope 被取消時蜈亩,它所跟蹤的所有協(xié)程都會被取消。上面的代碼中前翎,我使用了 GlobalScope稚配,正如我們不推薦隨意使用全局變量一樣,這種方式通常不推薦使用港华。所以药有,如果想要使用協(xié)程,您要么限定一個作用域 (scope),要么獲得一個作用域的訪問權(quán)限愤惰。而在 ViewModel 中,我們可以使用 viewModelScope 來管理協(xié)程的作用域赘理。
viewModelScope
當(dāng) ViewModel 被銷毀時宦言,通常都會有一些與其相關(guān)的操作也應(yīng)當(dāng)被停止。
例如商模,假設(shè)您正在準(zhǔn)備將一個位圖 (bitmap) 顯示到屏幕上奠旺。這種操作就符合我們前面提到的一些特征: 既不能在執(zhí)行時阻塞主線程,又要求在用戶退出相關(guān)界面時停止執(zhí)行施流。使用協(xié)程進(jìn)行此類操作時响疚,就應(yīng)當(dāng)使用 viewModelScope。
viewModelScope 是一個 ViewModel 的 Kotlin 擴(kuò)展屬性瞪醋。正如前面所說忿晕,它能在 ViewModel 銷毀時 (onCleared() 方法調(diào)用時) 退出。這樣一來银受,只要您使用了 ViewModel践盼,您就可以使用 viewModelScope 在 ViewModel 中啟動各種協(xié)程,而不用擔(dān)心任務(wù)泄漏宾巍。
示例如下:
class MyViewModel() : ViewModel() {
fun initialize() {
viewModelScope.launch {
processBitmap()
}
}
suspend fun processBitmap() = withContext(Dispatchers.Default) {
// 在這里做耗時操作
}
}
詳細(xì)了解更多咕幻,請參閱:
-
文章: 更簡便地在 Android 中使用協(xié)程: viewModelScope
https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471 -
官方文檔: 將 Kotlin 協(xié)程與架構(gòu)組件一起使用
https://developer.android.google.cn/topic/libraries/architecture/coroutines -
視頻演講: 理解 Android 中的 Kotlin 協(xié)程
https://v.youku.com/v_show/id_XNDE3NTM4OTQ2MA==.html
總結(jié)
本文中,我們講了:
- ViewModel 使用 SaveStateHandle 組件處理 onSaveInstanceState 相關(guān)邏輯顶霞;
- 通過配合 View Model 和導(dǎo)航圖來精確限定數(shù)據(jù)在 Fragment 中的共享范圍肄程;
- 使用 DataBinding 庫時,可以將 ViewModel 傳遞給數(shù)據(jù)綁定 (binding)选浑,如果同時有在 ViewModel 中使用 LiveData蓝厌,則可以通過 binding.setLifecycleOwner(lifecycleOwner) 讓 UI 根據(jù) LiveData 自動更新;
- 在 ViewModel 中使用 Kotlin 協(xié)程時鲜侥,使用 viewModelScope 來讓協(xié)程在 ViewModel 被銷毀時自動取消褂始。
以上這些功能很多都來自社區(qū)提交的請求和反饋,如果您正在尋找 ViewModel 相關(guān)的功能描函,可以留意功能需求列表或者考慮提交自己的需求崎苗。
如果您想了解架構(gòu)組件和 Android Jetpack 的最新進(jìn)展,請關(guān)注 Android 開發(fā)者博客舀寓,并留意 AndroidX 發(fā)布文檔胆数。
如果您對這些功能仍有疑問,可以在下方留言互墓。感謝閱讀必尼!
點(diǎn)擊這里查看 Android 官方中文文檔 —— ViewModel 概覽