知识点 | ViewModel 四种集成方式
官方文档 https://developer.android.google.cn/topic/libraries/architecture/viewmodel
ViewModel 中的 Saved State —— 后台进程重启时,ViewModel 的数据恢复; 在 NavGraph 中使用 ViewModel —— ViewModel 与导航 (Navigation) 组件库的集成; ViewModel 配合数据绑定 (data-binding) —— 通过使用 ViewModel 和 LiveData 简化数据绑定; viewModelScope —— Kotlin 协程与 ViewModel 的集成。
ViewModel 的 Saved State —— 后台进程重启时,ViewModel 的数据恢复
于 lifecycle-viewmodel-savedstate 的 1.0.0-alpha01 版本时加入 支持 Java 和 Kotlin
从当前界面永久离开: 用户导航至其他界面或直接关闭 Activity (通过点击返回按钮或执行的操作调用了 finish() 方法)。对应 Activity 实例被永久关闭; Activity 配置 (configuration) 被改变: 例如,旋转屏幕等操作,会使 Activity 需要立即重建; 应用在后台时,其进程被系统杀死: 这种情况发生在设备剩余运行内存不足,系统又亟须释放一些内存的时候。当进程在后台被杀死后,用户又返回该应用时,Activity 也需要被重建。
ViewModels: 持久化、onSaveInstanceState()、恢复 UI 状态与加载器 https://medium.com/androiddevelopers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090
Saved State 模块
现在,ViewModel Saved State 模块将会帮您在应用进程被杀死时恢复 ViewModel 的数据。在免除了与 Activity 繁琐的数据交换后,ViewModel 也真正意义上的做到了管理和持有所有自己的数据。
ViewModel 的这一新功能是通过 SavedStateHandle 实现的。SavedStateHandle 和 Bundle 一样,以键值对形式存储数据,它包含在 ViewModel 中,并且可以在应用处于后台时进程被杀死的情况下幸存下来。诸如用户 id 等需要在 onSaveInstanceState 时得到保存下来的数据,现在都可以存在 SavedStateHandle 中。
ViewModel Saved State
https://developer.android.google.cn/topic/libraries/architecture/viewmodel-savedstate
SavedStateHandle
https://developer.android.google.cn/reference/androidx/lifecycle/SavedStateHandle.html
现在让我们看看如何使用 SaveState 组件。注意接下来的代码会和 Lifecycles Codelab 第六步中的一段代码十分相似。那段是 Java 代码,而接下来的是 Kotlin 代码:
第一步: 添加依赖
SaveStateHandle 目前在一个独立的模块中,您需要在依赖中添加:
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
Lifecycles Codelab
https://codelabs.developers.google.com/codelabs/android-lifecycles/#6
lifecycle 版本发布文档
https://developer.android.google.cn/jetpack/androidx/releases/lifecycle#viewmodel-savedstate-1.0.0-alpha01
//下面的 Kotlin 扩展需要依赖以下或更新新版本的 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)
class MyViewModel(state :SavedStateHandle) :ViewModel() {
// 将Key声明为常量
companion object {
private val USER_KEY = "userId"
}
private val savedStateHandle = state
fun saveCurrentUser(userId: String) {
// 存储 userId 对应的数据
savedStateHandle.set(USER_KEY, userId)
}
fun getCurrentUser(): String {
// 从 saveStateHandle 中取出当前 userId
return savedStateHandle.get(USER_KEY)?: ""
}
}
构造方法: SavedStateHandle 作为构造方法参数传入 MyViewModel;
保存: saveNewUser 方法展示了使用键值对的形式保存 USER_KEY 和 userId 到 SaveStateHandle 的例子。每当数据更新时,要保存新的数据到 SavedStateHandle; 获取: 如代码中所示,调用 savedStateHandle.get(USER_KEY) 方法获取被保存的 userId。
// getLiveData 方法会取得一个与 key 相关联的 MutableLiveData
// 当与 key 相对应的 value 改变时 MutableLiveData 也会更新。
private val _userId : MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY)
// 只暴露一个不可变 LiveData 的情况
val userId : LiveData<String> = _userId
SavedStateHandle.getLiveData() https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/SavedStateHandle.html#getlivedata Lifecycles Codelab 第六步 https://codelabs.developers.google.com/codelabs/android-lifecycles/#6 官方文档 https://developer.android.google.cn/topic/libraries/architecture/viewmodel-savedstate
ViewModel 与 Jetpack 导航: 在 NavGraph 中使用 ViewModel
于 navigation 的 2.1.0-rc01 版本时加入
支持 Java 与 Kotlin
Jetpack 导航组件 (Navigation) 十分适用于那些只有少量或一个 Activity,但是 Activity 中会包含多个 Fragment 的应用。Ian Lake 在他的演讲: 单 Activity 架构: 为什么、什么情况下以及如何使用中介绍了一些我们选择单一 Activity 架构的原因,而与本文相关的一点,是这种架构允许在多个界面 (destination) 间共享 ActivityViewModel。您可以用 Activity 创建一个 ViewModel 实例,然后从这个 Activity 中的任一个 Fragment 中获得 ViewModel 的引用:
// 在Fragment的 onCreate 或 onActivityCreated 方法中执行
// 这个Kotlin扩展需要依赖最KTX库:androidx.fragment:fragment-ktx:1.1.0
val sharedViewModel: ActivityViewModel by activityViewModels()
单 Activity 架构: 为什么、什么情况下以及如何使用
https://v.youku.com/v_show/id_XMzkxMjM0NzQyOA==.html
△ 包含一些购买支付流程的导航图 (Navigation Graph)
Navigation 2.1.0 中引入了依托一个导航图 (navigation graph) 创建 ViewModel 的功能。在使用时,您需要先把一个界面集合 (例如: 登录流程、支付流程的相关界面),放到一个嵌套导航图 (nested navigation graph) 中。此时再通过嵌套导航图创建出 ViewModel,便可以在相关界面中共享数据了。
Navigation 2.1.0 https://developer.android.google.cn/jetpack/androidx/releases/navigation#2.1.0-alpha02 嵌套导航图 (nested navigation graph) https://developer.android.google.cn/guide/navigation/navigation-nested-graphs
△ 创建嵌套导航图的截图
<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>
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置其他 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
}
需要注意的是,嵌套导航图相对于导航图的其他部分是一个独立的整体。您无法导航至嵌套导航图中包含的某个特定界面;当您导航至一个嵌套导航图时,打开的只会是其中的开始界面 (startDestination)。这种特性使得嵌套导航图适合用于封装特定流程的界面组合,比如前面提到过的登录和支付流程。
ViewModel 与 NavGraph 的集成,是 2019 年 I/O 大会所发布的关于 Navigation 框架的新特性之一。
主题演讲: Jetpack Navigation 的主题演讲
https://v.youku.com/v_show/id_XNDE3NzAzNzQ4NA==.html
官方文档: 以编程方式与导航组件交互
https://developer.android.google.cn/guide/navigation/navigation-programmatic#share_
ViewModel 与 Data Binding: 在 Data Binding 中使用 ViewModel 和 LiveData
于 Android Studio 的 3.1 版本时加入
支持 Java 与 Kotlin
ViewModel、LiveData 与 Data Binding 的集成方式并不是什么新功能,但它始终非常好用。ViewModel 通常都包含一些 LiveData,而 LiveData 意味着可以被监听。所以最常见的使用场景是在 Fragment 中给 LiveData 添加一个观察者:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
myViewModel.name.observe(this, { newName ->
// 更新UI,这里是一个TextView
nameTextView.text = newName
})
}
Data Binding https://developer.android.google.cn/topic/libraries/data-binding
假设您希望在 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>
class MainActivity : AppCompatActivity() {
// 这个ktx扩展需要依赖 androidx.activity:activity-ktx:1.0.0
// 或更新版本
private val myViewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//填充视图并创建 Data Binding 对象
val binding: MainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.main_activity)
//声明这个 Activity 为 Data Binding 的 lifecycleOwner
binding.lifecycleOwner = this
// 将 ViewModel 传递给 binding
binding.viewmodel = myViewModel
}
}
现在,您可以像下面这样使用 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>
ViewMode 与 Kotlin 协程: viewModelScope
于 Lifecycle 的 2.1.0 版本时加入
只支持 Kotlin
通常情况下,我们使用回调 (Callback) 处理异步调用,这种方式在逻辑比较复杂时,会导致回调层层嵌套,代码也变得难以理解。Kotlin 协程 (Coroutines) 同样适用于处理异步调用,它让逻辑变得简单的同时,也确保了操作不会阻塞主线程。如果您不了解协程,这里有一系列很棒的博客《在 Android 开发中使用协程》以及 codelab: 在 Android 应用中使用 Kotlin 协程以供参考。
Kotlin 协程 (Coroutines) https://kotlinlang.org/docs/reference/coroutines-overview.html 在 Android 应用中使用 Kotlin 协程 https://codelabs.developers.google.com/codelabs/kotlin-coroutines/#0
// 下面是示例代码,真实情景下不要使用 GlobalScope
GlobalScope.launch {
longRunningFunction()
anotherLongRunningFunction()
}
CoroutineScope https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html GlobalScope https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/
viewModelScope
当 ViewModel 被销毁时,通常都会有一些与其相关的操作也应当被停止。
viewModelScope
https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary.html#(androidx.lifecycle.ViewModel).viewModelScope:kotlinx.coroutines.CoroutineScope
onCleared()
https://developer.android.google.cn/reference/androidx/lifecycle/ViewModel#onCleared()
示例如下:
class MyViewModel() : ViewModel() {
fun initialize() {
viewModelScope.launch {
processBitmap()
}
}
suspend fun processBitmap() = withContext(Dispatchers.Default) {
// 在这里做耗时操作
}
}
详细了解更多,请参阅:
文章: 更简便地在 Android 中使用协程: viewModelScope https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471 官方文档: 将 Kotlin 协程与架构组件一起使用 https://developer.android.google.cn/topic/libraries/architecture/coroutines 视频演讲: 理解 Android 中的 Kotlin 协程 https://v.youku.com/v_show/id_XNDE3NTM4OTQ2MA==.html
总结
ViewModel 使用 SaveStateHandle 组件处理 onSaveInstanceState 相关逻辑;
通过配合 View Model 和导航图来精确限定数据在 Fragment 中的共享范围;
使用 DataBinding 库时,可以将 ViewModel 传递给数据绑定 (binding),如果同时有在 ViewModel 中使用 LiveData,则可以通过 binding.setLifecycleOwner(lifecycleOwner) 让 UI 根据 LiveData 自动更新;
在 ViewModel 中使用 Kotlin 协程时,使用 viewModelScope 来让协程在 ViewModel 被销毁时自动取消。
以上这些功能很多都来自社区提交的请求和反馈,如果您正在寻找 ViewModel 相关的功能,可以留意功能需求列表或者考虑提交自己的需求。
如果您想了解架构组件和 Android Jetpack 的最新进展,请关注 Android 开发者博客,并留意 AndroidX 发布文档。
如果您对这些功能仍有疑问,可以在下方留言。感谢阅读!
功能需求列表
https://issuetracker.google.com/issues?q=status:open%20componentid:413132%20type:feature_request
提交自己的需求
https://issuetracker.google.com/issues/new?component=413132&template=1096619
Android 开发者博客
https://medium.com/androiddevelopers
AndroidX 发布文档
https://developer.android.google.cn/jetpack/androidx/releases
推荐阅读