随着 Android 架构的演进,从 MVC 到 MVP 再到现在的 MVVM,项目的结构越来越清晰,耦合度也越来越低,本质上讲就是对 UI 和逻辑的分离,而在这一分离的过程中,MVP 的 presenter 和 MVVM 中的ViewModel 都起了很重要的作用,Presenter 不必多说,就是一个类封装了我们的逻辑代码,并加了一些回调。我们要讲的是 ViewModel 如何创建使用,如何和页面生命周期绑定以及如何在配置更改时恢复数据。
1.what?
ViewModel 和 LiveData 是组成 Jetpack 的一部分,在 MVVM 架构中充当着相当重要的角色。
ViewModel 旨在以注重生命周期的方式存储和管理界面相关的数据,让数据可在发生屏幕旋转等配置更改后继续留存,所以 ViewModel 在 MVVM 中担当的是一个数据持有者的角色,为 Activity 、Fragment 存储数据,在配置更改的时候恢复数据,其次因为 ViewModel 存储了数据,所以 ViewModel 可以在当前 Activity的 Fragment 中实现数据共享。
LiveData 作为ViewModel的好基友,是一个可以感知 Activity 、Fragment生命周期的数据容器。当 LiveData所持有的数据改变时,它会通知相应的组件进行更新。同时,LiveData 持有界面代码 Lifecycle的引用,这意味着它会在界面代码(LifecycleOwner)的生命周期处于 started 或 resumed 时作出相应更新,而在 LifecycleOwner 被销毁时停止更新。它的优点:不用手动控制生命周期,不用担心内存泄露,数据变化时会收到通知.
2.How?
2.1 基本用法
我们先看看 ViewModel 是怎么使用的(虽然大家都比较熟悉)。首先,我们创建一个ViewModel子类,类里面有一个 LiveData 对象:
class MyViewModel : ViewModel() {
val mNameLiveData = MutableLiveData()
}
然后我们在 Activity 里面使用它:
class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = findViewById<TextView>(R.id.textView)
val viewModel by viewModel<MyViewModel>()
viewModel.mNameLiveData.observe(this, Observer {
textView.text = it
})
}
}
例子非常简单,这里就不过多的介绍。需要提一句的是,在最新的ViewModel中,以前通过ViewModelProviders.of 方法来获取 ViewModel 已经废弃了,现在我们是通过 ViewModelProvider Factory 创建 ViewModel 对象,因此需要往 ViewModelProider 构造方法里面传递一个工厂类对象,如下:
class MyViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor().newInstance()
}
}
当然,我们可以不带 Factory 对象。那么加入 Factory 对象之后,相比较于以前有什么好处呢?加了 Factory 之后,我们可以定义构造方法带参的 ViewModel。比如说,我们的一个 ViewModel 构造方法需要带一个 id 参数,那么我们可以在 Factory 的 create 方法里面创建对象直接带进去。
我们还可以根据提供的参数使用 lazyMap 或类似的 lazy init。当参数是字符串或其他不可变类时,很容易将它们用作映射的键,以获取与提供的参数相对应的 LiveData。
class Books(val names: List<String>)
data class Parameters(val namePrefix: String = "")/*只为示范*/
class GetBooksCase {
fun loadBooks(parameters: Parameters, onLoad: (Books) -> Unit) { /* Implementation detail */
}
}
class BooksViewModel(val getBooksCase: GetBooksCase) : ViewModel() {
private val booksLiveData: Map<Parameters, LiveData<Books>> = lazyMap { parameters ->
val liveData = MutableLiveData<Books>()
getBooksCase.loadBooks(parameters) {
liveData.value = it
}
return@lazyMap liveData
}
fun books(parameters: Parameters): LiveData<Books> = booksLiveData.getValue(parameters)
}
fun <K, V> lazyMap(initializer: (K) -> V): Map<K, V> {
val map = mutableMapOf<K, V>()
return map.withDefault { key ->
val newValue = initializer(key)
map[key] = newValue
return@withDefault newValue
}
}
在上面使用 lazy map 的时候,我们只使用 map 来传递参数,但在许多情况下,ViewModel 的一个实例将始终具有相同的参数。这时候最好将参数传递给构造函数,并在构造函数中使用 lazy load 或 start load。
class BooksViewModel(val getBooksCase: GetBooksCase, parameters: Parameters) : ViewModel() {
private val booksLiveData: LiveData<Books> by lazy {
val liveData = MutableLiveData<Books>()
getBooksCase.loadBooks(parameters) {
liveData.value = it
}
return@lazy liveData
}
fun books(parameters: Parameters): LiveData<Books> = booksLiveData
}
class BooksViewModelFactory(val getBooksCase: GetBooksCase, val parameters: Parameters) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return BooksViewModel(getBooksCase, parameters) as T
}
}
切记,我们不要自己创建 ViewModel 对象,因为自己创建的对象不能保存因为配置更改导致 Activity 重建的数据,从而完美避开了 ViewModel 的优点。
2.2 DataBinding 中使用 ViewModel 和 LiveData
ViewModel、LiveData 与 DataBinding 并不是什么新功能,但非常好用(但因为一些 DataBinding 出了问题全局报错不好定位的原因,被众大佬诟病甚至弃用)。ViewModel 通常都包含一些 LiveData,而 LiveData 意味着可以被监听。在 XML 布局文件中使用ViewModel时,调用 binding.setLifecycleOwner(this) 方法,然后将 ViewModel 传递给 binding 对象,就可以将 LiveData 与 Data Binding 结合起来:
class MainActivity : AppCompatActivity() {
private val myViewModel: MyViewModel by lazy {
ViewModelProvider(
this,
MyViewModelFactory()
)[MyViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MainActivityBinding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.lifecycleOwner = this
// 将 ViewModel 传递给 binding
binding.viewmodel = myViewModel
}
}
XML 布局文件中使用 ViewModel:
<layout>
<data>
<variable
name="viewModel"
type="com.gxj.test.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.text}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
注意,这里的 viewModel.text 可以是 String 类型,也可以是 LiveData。如果它是 LiveData,那么 UI 将根据 LiveData 值的改变自动刷新。
2.3 ViewMode 与 Kotlin 协程: viewModelScope
通常情况下,我们使用回调 (Callback) 处理异步调用,这种方式在逻辑比较复杂时,会导致回调嵌套地狱,代码也变得难以理解。而协程同样适用于处理异步调用,它能够让逻辑变得简单的同时,也确保了操作不会阻塞主线程。一段简单的协程代码,真实情景下不要使用:
GlobalScope.launch {
longRunningFunction()
longRunningFunction1()
}
这段代码只启动了一个协程,但我们在真实的使用环境下很容易创建出许多协程,这就难免会导致有些协程的状态无法被跟踪。如果这些协程中刚好有您想要停止的任务时,就会导致任务泄漏。而为了防止任务泄漏,需要将协程加入到一个 CoroutineScope 中,它可以持续跟踪协程的执行,也可以被取消。当 CoroutineScope 被取消时,它所跟踪的所有协程都会被取消。上面的代码中,我使用了GlobalScope,正如我们不推荐随意使用全局变量一样,这种方式通常不推荐使用。所以,如果想要使用协程,要么限定一个作用域 (scope),要么获得一个作用域的访问权限。而在 ViewModel 中,我们可以使用 viewModelScope 来管理协程的作用域,它是一个ViewModel 的 kotlin 扩展属性,当 ViewModel 被销毁时,通常都会有一些与其相关的操作也应当被停止。
举个栗子,当我们要加载一个文件的时候: 既要做到不能在执行时阻塞主线程,又要求在退出相关界面时停止加载。当使用协程进行耗时操作时,就应当使用 viewModelScope, ,它能在 ViewModel 销毁时 (onCleared()方法调用时) 退出。这样我们就可以在 ViewModel 的 viewModelScope 中启动各种协程,而不用担心任务泄漏。
示例如下:
class MyViewModel() : ViewModel() {
fun initialize() {
viewModelScope.launch {
processLoadFile()
}
}
suspend fun processLoadFile() = withContext(Dispatchers.Default) {
// 在这里做耗时操作
}
}
2.4 ViewModel 的 Saved State
- onSaveInstanceState 带来的挑战
我们知道 Activity 和 Fragment 通常会在下面三种情况下被销毁:
- 从当前界面永久离开: 用户导航至其他界面或直接关闭 Activity (通过点击返回按钮或执行的操作调用了 finish() 方法)。对应 Activity 实例被永久关闭;
- Activity 配置被改变: 例如,旋转屏幕等操作,会使 Activity 需要立即重建;
- 应用在后台时,其进程被系统杀死: 这种情况发生在设备剩余运行内存不足,系统又亟须释放一些内存的时候。当进程在后台被杀死后,用户又返回该应用时,Activity 也需要被重建。
在后两种情况中,我们通常都希望重建 Activity。ViewModel 会处理第二种情况,因为在这种情况下 ViewModel 没有被销毁;而在第三种情况下, ViewModel 被销毁了。所以一旦出现了第三种情况,便需要在 Activity 的 onSaveInstanceState 相关回调中保存和恢复 ViewModel 中的数据。
- Saved State 模块
ViewModel 保存和恢复的数据范围仅限于配置更改导致的重建,并不支持因为资源限制导致 Activity 重建的情况。但是,大家对此的呼声却从来没有停歇,Google 因此新增了一个 SavedStateHandle 类,用来满足我们的要求。该模块会在应用进程被杀死时恢复 ViewModel 的数据。在免除了与 Activity 繁琐的数据交换后,ViewModel 也真正意义上的做到了管理和持有所有自己的数据。
SavedStateHandle 和 Bundle 一样,以键值对形式存储数据,它包含在 ViewModel 中,并且可以在应用处于后台时进程被杀死的情况下幸存下来。诸如用户 id 等需要在 onSaveInstanceState 时得到保存下来的数据,现在都可以存在 SavedStateHandle 中。
- 使用Save State模块
-
添加依赖
SaveStateHandle 目前在一个独立的模块中,所以需要在依赖中添加:
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0"
- 修改调用 ViewModelProvider 的方式
创建一个 SaveStateHandle 的ViewModel,在 onCreate() 方法中将 ViewModelProvider 的调用修改为:
class MainActivity : AppCompatActivity(R.layout.activity_main) {
val viewModel = ViewModelProvider(
this,
SavedStateViewModelFactory(application, this)
).get(MyViewModel::class.java)
}
创建 ViewModel 的类是 ViewModelFactory,而创建包含 SaveStateHandle 的 ViewModel 的工厂类是 SavedStateViewModelFactory。通过此工厂创建的 ViewModel 将持有一个基于传入 Activity 或 Fragment 的 SaveStateHandle。如果我们的 ViewModel 构造方法只带一个 SavedStateHandle 参数或者带有一个Application 参数和 SavedStateHandle 参数,可以直接使用 SavedStateViewModelFactory。如果构造方法还带有其他的参数,此时需要继承 AbstractSavedStateViewModelFactory 实现我们自己的工厂类。在使用AbstractSavedStateViewModelFactory 时,我们需要注意一点:create 方法带的 SavedStateHandle 参数一定传递到 ViewModel 里面去。
- 调用SaveStateHandle
举一个保存用户 ID 的例:
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)?: ""
}
}
保存: saveNewUser 方法展示了使用键值对的形式保存 USER_KEY 和 userId 到 SaveStateHandle 的例子。每当数据更新时,要保存新的数据到 SavedStateHandle;
获取: 调用 savedStateHandle.get(USER_KEY) 方法获取被保存的 userId。
现在,无论是第二还是第三种情况下,SavedStateHandle 都可以恢复界面数据。
3.why?
3.1 ViewModel 是如何创建的?
ViewModelProivder 有很多构造方法,不过最终都调到同一个地方:
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
这个方法中,mFactory 就是我们预期的工厂类,用来创建 ViewModel 对象;mViewModelStore 是一个什么东西呢?这个很好理解,mViewModelStore 就是用来存储的 ViewModel 对象的,比如同一个 Activity 的onCreate() 方法可能会多次回调,我们在 onCreate()方法初始化ViewModel,但是不可能每次 onCreate() 回调都会创建新的 ViewModel 对象,所以需要有一个东西用来存储的我们之前创建过的 ViewModel,这个就是ViewModelStore 的作用。而 ViewModel 生命周期比 Activity 的生命周期长也是因为这个类。
那么 mViewModelStore 对象是从哪里传过来,我们清楚的记得构造方法里面我们并没有传这个变量。
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}
我们可以看到从 ViewModelStoreOwner 获取的,代码如下
public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}
ViewModelStoreOwner是一个接口,那么哪些类是这个借口的实现类呢?如你所料,我们熟悉的ComponentActivity 和 Fragment 都实现了这个接口。
我们再来看一下 get 方法,因为真正获取 ViewModel 对象就是通过这个方法的。
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
这个get方法没有做什么事情,构造了一个默认的 key,然后调用另一个 get 方法。代码如下:
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
这个 get 方法总的来说,主要分为以下2个过程:
- 先通过 key 从 ViewModelStore (缓存)获取 ViewModel 对象,如果缓存中存在,直接返回。Activity 经过横屏重建之后,返回 ViewMode 的对象就是这里返回。
- 如果缓存不存在,那么通过 Factory 创建一个对象,然后放在缓存中,最后返回。
3.2.ViewModel 如何做到配置更改时依然可以恢复数据?
在上面讲SaveState的时候,提到了Activity 和 Fragment 被销毁的三种情况,在这三种情况下的 ViewModel 的生命周期可以看下图:
从这张图里面,我们可以看出,ViewModel 的生命周期要比Activity长一点。ViewModel 存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。在此期间ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。
在前面的概述中,我们已经知道 ViewModel 的生命周期要比 Activity 长一点。那 ViewModel 是怎么做到的呢?对于这个问题,我猜大家首先想到的是缓存,并且这个缓存是被 static 关键字修饰的。正常来说,这个实现方案是没有问题的,我们也能找到具体的例子,比如 Eventbus 就是这么实现的。
那么在 ViewModel 中,这个是怎么实现的呢?我们都知道 ViewModel 是从一个 ViewModelStore 缓存里面的获取,我们看了 ViewModelStore 的源码,发现它的内部并没有通过静态缓存实现。那么它是怎么实现Activity 在 onDestroy 之后(重建),还继续保留已有的对象呢?
这个我们可以从 ComponentActivity 的 getViewModelStore 方法去寻找答案:
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
getViewModeStrore 方法的目的很简单,就是获取一个 ViewModelStrore 对象。那么这个 ViewModelStore 可以从哪里获取呢?我们从上面的代码中可以找到两个地方:
- 从 NonConfigurationInstances 获取。
- 创建一个新的 ViewModelStore 对象。
第二点我们不用看,关键是 NonConfigurationInstances。NonConfigurationInstances 这是什么东西?
NonConfigurationInstances 其实就是一个 Wrapper,用来包装一下因为不受配置更改影响的数据,包括我们非常熟悉的 Fragment,比如说,一个 Activity 上面有一个 Fragment,旋转了屏幕导致 Activity 重新创建,此时Activity 跟之前的不是同一个对象,但是 Fragment 却是同一个,这就是通过 NonConfigurationInstances 实现的。也就是说在 getViewModelStore 方法里面,从 NonConfigurationInstances 获取的 ViewModelStore 对象其实就是上一个 Activity 的。同时,我们还可以在 ComponentActivity 里面看到一段代码:
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_STOP) {
Window window = getWindow();
final View decor = window != null ? window.peekDecorView() : null;
if (decor != null) {
decor.cancelPendingInputEvents();
}
}
}
});
从上面的代码中,我们可以到如果 Activity 是因为配置更改导致 onDestroy 方法的回调,并不会清空ViewModelStore 里面的内容,这就能保证当 Activity 因为配置更改导致重建重新创建的 ViewModel 对象跟之前创建的对象是同一个。反之,如果 Activity 是正常销毁的话,则不会保存之前创建的 ViewModel 对象,对应的是 ViewModelStore 的 clear 方法调用。其实这个 clear 方法还跟 kotlin 里面的协程有关,这里就不过多解释了,有兴趣的同学可以看看 ViewModel.viewModelScope。
现在我们来看一下 NonConfigurationInstances 为啥能保证 Activity 重建前后,ViewModeStore 是同一个对象呢?我们直接从ActivityThread的performDestroyActivity方法去寻找答案。我们知道,performDestroyActivity 方法最后会回调到 Activity 的 onDestroy 方法,我们可以通过这个方法可以找到ActivtyThread 在 Activity onDestroy 之前做了保存操作。
ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance, String reason) {
// ······
performPauseActivityIfNeeded(r, "destroy");
// Activity的onStop方法回调
if (!r.stopped) {
callActivityOnStop(r, false /* saveState */, "destroy");
}
if (getNonConfigInstance) {
// ······
// retainNonConfigurationInstances方法的作用就是创建一个对象
r.lastNonConfigurationInstances= r.activity.retainNonConfigurationInstances();
// ······
}
// ······
// Activity的onDestroy方法回调
mInstrumentation.callActivityOnDestroy(r.activity);
// ······
return r;
}
从上面的代码中看出,在 Activity 的 onStop 和 onDestroy之间,会回调 retainNonConfigurationInstances方法,同时记录到ActivityClientRecord中去。这里retainNonConfigurationInstances 方法返回的对象就是我们之前看到的 NonConfigurationInstances 对象。
那么又在哪里恢复已保存的 NonConfigurationInstances 对象呢?这个可以从 performLaunchActivity 方法找到答案。performLaunchActivity 方法的作用就是启动一个 Activity,Activity 重建肯定会调用这个方法。在performLaunchActivity方法里面,调用了Activity的attach方法,在这个方法,Google将已有的NonConfigurationInstances 赋值给了新的 Activity 对象。
到这里,我们就知道为啥 NonConfigurationInstances 能保证 ViewModelStore 在 Activity 重建前后是同一个对象,同时也知道为啥 ViewModel 的生命周期比 Activity 的生命周期要长一点。
总结
在本篇文章中我讲述了什么是 ViewModel,如何传递参数到 ViewModel 中去,以及 ViewModel一些使用场景,也相信大家对 ViewModel 都能立马上手了。接着我们又从源码的角度分析了 ViewModel 是如何创建的,是如何和 Activity 的生命周期绑定在一起的,这让我们能够更深入的理解 ViewModel,最后讲述了 ViewModel 在配置更改以及销毁重建时是如何保存和恢复数据的。ViewModel 作为数据的处理和分发者,在 MVVM 盛行的当下承扮演着越来越重要的角色,让我们把ViewModel深入提炼并应用到实际项目中吧!