Android Jetpack系列之ViewModel
1.导入依赖
2. ViewModel
生命周期与activity,多fragment数据共享
ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。
Android 框架可以管理界面控制器(如 Activity 和 Fragment)的生命周期。Android 框架可能会决定销毁或重新创建界面控制器,以响应完全不受您控制的某些用户操作或设备事件。
如果系统销毁或重新创建界面控制器,则存储在其中的任何瞬态界面相关数据都会丢失。例如,应用可能会在它的某个 Activity 中包含用户列表。为配置更改重新创建 Activity 后,新 Activity 必须重新提取用户列表。对于简单的数据,Activity 可以使用 onSaveInstanceState() 方法从 onCreate() 中的捆绑包恢复其数据,但此方法仅适合可以序列化再反序列化的少量数据,而不适合数量可能较大的数据,如用户列表或位图。
另一个问题是,界面控制器经常需要进行可能需要一些时间才能返回的异步调用。界面控制器需要管理这些调用,并确保系统在其销毁后清理这些调用以避免潜在的内存泄漏。此项管理需要大量的维护工作,并且在为配置更改重新创建对象的情况下,会造成资源的浪费,因为对象可能需要重新发出已经发出过的调用。
诸如 Activity 和 Fragment 之类的界面控制器主要用于显示界面数据、对用户操作做出响应或处理操作系统通信(如权限请求)。如果要求界面控制器也负责从数据库或网络加载数据,那么会使类越发膨胀。为界面控制器分配过多的责任可能会导致单个类尝试自己处理应用的所有工作,而不是将工作委托给其他类。以这种方式为界面控制器分配过多的责任也会大大增加测试的难度。
从界面控制器逻辑中分离出视图数据所有权的操作更容易且更高效。
3.实现 ViewModel
架构组件为界面控制器提供了 ViewModel 辅助程序类,该类负责为界面准备数据。在配置更改期间会自动保留 ViewModel 对象,以便它们存储的数据立即可供下一个 activity 或 fragment 实例使用。例如,如果您需要在应用中显示用户列表,请确保将获取和保留该用户列表的责任分配给 ViewModel,而不是 activity 或 fragment,如以下示例代码所示:
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
然后,您可以从 Activity 访问该列表,如下所示:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
// Use the 'by viewModels()' Kotlin property delegate
// from the activity-ktx artifact
//需要引入依赖 api "androidx.activity:activity-ktx:${activityKtx}"
val model: MyViewModel by viewModels()
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
}
}
如果重新创建了该 Activity,它接收的 MyViewModel 实例与第一个 Activity 创建的实例相同。当所有者 Activity 完成时,框架会调用 ViewModel 对象的 onCleared() 方法,以便它可以清理资源。
打印日志
第一次打开activity,在onCreate里打印 activity hasCode:245395765 viewModel hasCode:211520205
关闭后重新打开,activity和viewModel都变了
activity hasCode:83192527 viewModel hasCode:259847308
不关闭activity只旋转屏幕,activity重建了,但viewModel还是原来的对象
activity hasCode:208237309 viewModel hasCode:259847308
//继续旋转也是一样,viewModel还是原来的对象
activity hasCode:151657118 viewModel hasCode:259847308
4.ViewModel 的生命周期
ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 activity,是在 activity 完成时;而对于 fragment,是在 fragment 分离时。
图 1 说明了 Activity 经历屏幕旋转而后结束时所处的各种生命周期状态。该图还在关联的 Activity 生命周期的旁边显示了 ViewModel 的生命周期。此图表说明了 Activity 的各种状态。这些基本状态同样适用于 Fragment 的生命周期。
您通常在系统首次调用 Activity 对象的 onCreate() 方法时请求 ViewModel。系统可能会在 activity 的整个生命周期内多次调用 onCreate(),如在旋转设备屏幕时。ViewModel 存在的时间范围是从您首次请求 ViewModel 直到 activity 完成并销毁。
5.在 Fragment 之间共享数据
Activity 中的两个或更多 Fragment 需要相互通信是一种很常见的现象。想象一下拆分视图 (list-detail) Fragment 的常见情况,假设您有一个 Fragment,在该 Fragment 中,用户从列表中选择一项,还有另一个 Fragment,用于显示选定项的内容。这种情况不太容易处理,因为这两个 Fragment 都需要定义某种接口描述,并且所有者 Activity 必须将两者绑定在一起。此外,这两个 Fragment 都必须处理另一个 Fragment 尚未创建或不可见的情况。
可以使用 ViewModel 对象解决这一常见的难点。这两个 fragment 可以使用其 activity 范围共享 ViewModel 来处理此类通信,如以下示例代码所示:
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".viewModel.TestViewModelActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/com.example.android_kotlin.viewModel.ListFragment"
android:layout_width="match_parent"
android:layout_height="200dp"
android:tag="com.example.android_kotlin.viewModel.ListFragment"
android:name="com.example.android_kotlin.viewModel.ListFragment"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/com.example.android_kotlin.viewModel.DetailFragment"
android:layout_width="match_parent"
android:layout_height="200dp"
android:tag="com.example.android_kotlin.viewModel.DetailFragment"
android:name="com.example.android_kotlin.viewModel.DetailFragment" />
</LinearLayout>
class TestViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_view_model)
//可以不在activity里初始化这一个,
//也能直接在fragment里共享SharedViewModel,一般是不在activity里初始化SharedViewModel
val sharedViewModel:SharedViewModel by viewModels()
LogUtils.d("sharedViewModel hasCode:${sharedViewModel.hashCode()}")
}
}
class ListFragment : Fragment() {
private lateinit var itemChangeButton: Button
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
private lateinit var dataBinding: FragmentListLayoutBinding;
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dataBinding = DataBindingUtil.inflate<FragmentListLayoutBinding>(
inflater,
R.layout.fragment_list_layout,
container,
false
);
LogUtils.d("model hasCode:${model.hashCode()}")
return dataBinding.root;
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dataBinding.btnChangeValue.setOnClickListener {
model.select(TimeUtils.date2String(Date()))
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
private lateinit var dataBinding:FragmentDetailLayoutBinding;
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dataBinding = DataBindingUtil.inflate<FragmentDetailLayoutBinding>(inflater, R.layout.fragment_detail_layout,container,false)
return dataBinding.root;
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
LogUtils.d("onViewCreated")
LogUtils.d("model hasCode:${model.hashCode()}")
model.selected.observe(viewLifecycleOwner, Observer<String> { item ->
// Update the UI
dataBinding.textViewChangeValue.setText(item)
})
}
}
请注意,这两个 Fragment 都会检索包含它们的 Activity。这样,当这两个 Fragment 各自获取 ViewModelProvider 时,它们会收到相同的 SharedViewModel 实例(其范围限定为该 Activity)。
此方法具有以下优势:
- Activity 不需要执行任何操作,也不需要对此通信有任何了解。
- 除了
SharedViewModel约定之外,Fragment 不需要相互了解。如果其中一个 Fragment 消失,另一个 Fragment 将继续照常工作。 - 每个 Fragment 都有自己的生命周期,而不受另一个 Fragment 的生命周期的影响。如果一个 Fragment 替换另一个 Fragment,界面将继续工作而没有任何问题。
6.将CursorLoader加载器替换为 ViewModel
7.将协程与 ViewModel 一起使用
8.更多信息
ViewModel ,onSaveInstanceState/onRestoreInstanceState和持久化保存状态的区别。
随着数据变得越来越复杂,您可能会选择使用单独的类加载数据。ViewModel 的用途是封装界面控制器的数据,以使数据在配置更改后仍然存在。如需了解如何在配置更改后加载、保留和管理数据,请参阅保存界面状态。
Android 应用架构指南建议构建存储区类来处理这些功能。
9.ViewModel 与 onSaveInstanceState,onRestoreInstanceState
ViewModel 是存储在内存里的,但onSaveInstanceState是已序列化到磁盘的一些简单信息(详见这里),所以有时候需要从onSaveInstanceState保存的信息里还原信息到ViewModel,
SavedStateHandle可以封装类似于onSaveInstanceState和onRestoreInstanceState,的方法到ViewModel里。
9.1 定义
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
9.2使用
class MainFragment : Fragment() {
//会自动接收到SavedStateHandle
val vm: SavedStateViewModel by viewModels()
...
}
10.ViewModel与协程
10.1导入依赖
对于 ViewModelScope,请使用 androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 或更高版本。
生命周期感知型协程范围
10.2 ViewModelScope
为应用中的每个 ViewModel 定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在 ViewModel 处于活动状态时才需要完成的工作,此时协程非常有用。例如,如果要为布局计算某些数据,则应将工作范围限定至 ViewModel,以便在 ViewModel 清除后,系统会自动取消工作以避免消耗资源。
您可以通过 ViewModel 的 viewModelScope 属性访问 ViewModel 的 CoroutineScope,如以下示例所示:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
11.ViewModel与Repository
class WordViewModel(private val repository: WordRepository) : ViewModel() {
// Using LiveData and caching what allWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return WordViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
总结
viewModels在fragment或者activity里,
activity和fragment都有扩展 viewModels方法。 val sharedViewModel:SharedViewModel by viewModels()
多个子fragment共享activity里的viewModel
activityViewModels是Fragment的扩展方法,只能在fragment里调用。
private val model: SharedViewModel by activityViewModels()
导航图与viewModel
navGraphViewModels是fragment的扩展方法。
val viewModel = by navGraphViewModels(R.id.child_graph)