阅读 1321

【带着问题学】关于ViewModel你应该知道的知识点

前言--阅读本文你将得到什么?

随着Jetpack相关组件使用的越来越多,在面试中问到的概率也越来越大

本文主要包括以下内容

  • ViewModel的四种使用方式
  • 为什么Activity旋转屏幕后ViewModel可以恢复数据
  • 为什么屏幕旋转后Fragment中的ViewModel可以恢复数据
  • 为什么Fragment间可以通过ViewModel共享数据

随着Jetpack的发布,为我们开发提供了不少便利,ViewModel就是其中之一。 ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。

ViewModel使用的四种方式

下面介绍下ViewModel的四种使用方式

  • ViewModel 中的 Saved State —— 后台进程重启时,ViewModel 的数据恢复;
  • 在 NavGraph 中使用 ViewModel —— ViewModel 与导航 (Navigation) 组件库的集成;
  • ViewModel 配合数据绑定 (data-binding) —— 通过使用 ViewModel 和 LiveData 简化数据绑定;
  • viewModelScope —— Kotlin 协程与 ViewModel 的集成。

下面介绍下我常用的ViewModel数据恢复与ViewModel与协程结合两种,更多的可见: 知识点 | ViewModel 四种集成方式

ViewModel 的 Saved State —— 后台进程重启时,ViewModel 的数据恢复

我们都知道,在屏幕旋转时,ViewModel可以保存数据。
但是当应用在后台进程被系统杀死,当重新打开页面时,ViewModel的数据并不会恢复.
这种情况就需要与SavedStateHandle结合,在后台进程回收时保存数据.

下面我们来看下如何使用SavedStateHandle

第一步: 添加依赖

SaveStateHandle 目前在一个独立的模块中,您需要在依赖中添加:

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version
复制代码
第二步: 修改调用 ViewModelProvider 的方式

接下来,您需要创建一个持有 SaveStateHandle 的 ViewModel。在 Activity 或 Fragment 的 onCreate 方法中,将 ViewModelProvider 的调用修改为:

//下面的 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)
复制代码

创建 ViewModel 的类是 ViewModel 工厂 (ViewModel factory),而创建包含 SaveStateHandle 的 View Model 的工厂类是 SavedStateViewModelFactory。通过此工厂创建的 ViewModel 将持有一个基于传入 Activity 或 Fragment 的 SaveStateHandle。

第三步: 使用 SaveStateHandle

当前面的步骤准备完成时,您就可以在 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 对应的数据
        savedStateHandle.set(USER_KEY, userId)
    }

    fun getCurrentUser(): String {
        // 从 saveStateHandle 中取出当前 userId
        return savedStateHandle.get(USER_KEY)?: ""
    }
}
复制代码

如果您想要在 ViewModel 中使用 LiveData,可以调用SavedStateHandle.getLiveData(),示例如下:

// getLiveData 方法会取得一个与 key 相关联的 MutableLiveData 
// 当与 key 相对应的 value 改变时 MutableLiveData 也会更新。
private val _userId : MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY)

// 只暴露一个不可变 LiveData 的情况
val userId : LiveData<String> = _userId
复制代码

ViewMode 与 Kotlin 协程: viewModelScope

通常情况下,我们使用回调 (Callback) 处理异步调用,这种方式在逻辑比较复杂时,会导致回调层层嵌套,代码也变得难以理解。Kotlin 协程 (Coroutines)同样适用于处理异步调用,它让逻辑变得简单的同时,也确保了操作不会阻塞主线程

一段简单的协程代码:

// 下面是示例代码,真实情景下不要使用 GlobalScope 
GlobalScope.launch {
    longRunningFunction()
    anotherLongRunningFunction()
}
复制代码

这段示例代码只启动了一个协程,但我们在真实的使用环境下很容易创建出许多协程,这就难免会导致有些协程的状态无法被跟踪。如果这些协程中刚好有您想要停止的任务时,就会导致任务泄漏(work leak)。

为了防止任务泄漏,您需要将协程加入到一个CoroutineScope中。CoroutineScope 可以持续跟踪协程的执行,它可以被取消。当 CoroutineScope 被取消时,它所跟踪的所有协程都会被取消。上面的代码中,我使用了GlobalScope,正如我们不推荐随意使用全局变量一样,这种方式通常不推荐使用。所以,如果想要使用协程,您要么限定一个作用域 (scope),要么获得一个作用域的访问权限。而在 ViewModel 中,我们可以使用 viewModelScope 来管理协程的作用域。

viewModelScope

当 ViewModel 被销毁时,通常都会有一些与其相关的操作也应当被停止。

例如,假设您正在准备将一个位图 (bitmap) 显示到屏幕上。这种操作就符合我们前面提到的一些特征: 既不能在执行时阻塞主线程,又要求在用户退出相关界面时停止执行。使用协程进行此类操作时,就应当使用viewModelScope。

viewModelScope 是一个 ViewModel 的 Kotlin 扩展属性。正如前面所说,它能在 ViewModel 销毁时 (onCleared()方法调用时) 退出。这样一来,只要您使用了 ViewModel,您就可以使用 viewModelScope 在 ViewModel 中启动各种协程,而不用担心任务泄漏

class MyViewModel() : ViewModel() {

    fun initialize() {
        viewModelScope.launch {
            processBitmap()
        }
    }

    suspend fun processBitmap() = withContext(Dispatchers.Default) {
        // 在这里做耗时操作
    }

}
复制代码

ViewModel为什么旋转时可以保存数据?

我们都知道 ViewModel 不会因为 Activity 的旋转发生改变而销毁,ViewModel 作用域如下所示: 观察上图,我相信小伙伴们肯定有如下疑惑: 当 Activity 因旋转发生改变时,系统会重新创建一个新的 Activity 。那老的 Activity 中的 ViewModel 是如何传递给新的 Activity 的呢?

我们首先了解下数据恢复的几种常见方式

使用 onSaveInstanceState 与 onRestoreInstanceState

当您的 Activity 开始停止时,系统会调用 onSaveInstanceState() 方法,以便您的 Activity 可以将状态信息保存到实例状态 Bundle 中。

重建先前被销毁的 Activity 后,您可以从系统传递给 Activity 的 Bundle 中恢复保存的实例状态。onCreate() 和 onRestoreInstanceState() 回调方法均会收到包含实例状态信息的相同 Bundle。

使用 Fragment 的 setRetainInstance

当配置发生改变时,Fragment 会随着宿主 Activity 销毁与重建,当我们调用 Fragment 中的 setRetainInstance(true) 方法时,系统允许 Fragment 绕开销毁-重建的过程。使用该方法,将会发送信号给系统,让 Activity 重建时,保留 Fragment 的实例。需要注意的是:

  • 使用该方法后,不会调用 Fragment 的 onDestory() 方法,但仍然会调用 onDetach() 方法
  • 使用该方法后,不会调用 Fragment 的 onCreate(Bundle) 方法。因为 Fragment 没有被重建。
  • 使用该方法后,Fragment 的 onAttach(Activity) 与 onActivityCreated(Bundle) 方法仍然会被调用。

使用 onRetainNonConfigurationInstance 与 getLastNonConfigurationInstance

在 Activity 中提供了 onRetainNonConfigurationInstance 方法,用于处理配置发生改变时数据的保存。随后在重新创建的 Activity 中调用 getLastNonConfigurationInstance 获取上次保存的数据。我们不能直接重写上述方法,如果想在 Activity 中自定义想要恢复的数据,需要我们调用上述两个方法的内部方法:

  • onRetainCustomNonConfigurationInstance()
  • getLastCustomNonConfigurationInstance()

注意:onRetainNonConfigurationInstance 方法系统调用时机介于 onStop - onDestory 之间,getLastNonConfigurationInstance 方法可在 onCreate 与 onStart 方法中调用。

几种数据恢复方式的总结

通过了解数据恢复的几种方式,我们能得到如下对比图:

ViewModel 的恢复

ViewModel 在官方设计之初就倾向于在配置改变时进行数据的恢复。考虑到数据恢复时的效率,官方最终采用了 onRetainNonConfigurationInstance 的方式来恢复 ViewModel 。

知道了 ViewModel 的恢复方式,那现在一起来解决我们之前的疑惑。当 Activity 因配置发生改变时,系统会重新创建一个新的 Activity 。那老的 Activity 中的 ViewModel 是如何传递给新的 Activity ?

在 Androidx 中的 Activity 的最新代码中,官方重写了 onRetainNonConfigurationInstance 方法,在该方法中保存了 ViewModelStore (ViweModelStore 中存储了 ViewModel ),进而也保存了 ViewModel,具体代码如下所示:

    public final Object onRetainNonConfigurationInstance() {
        Object custom = onRetainCustomNonConfigurationInstance();

        ViewModelStore viewModelStore = mViewModelStore;
        if (viewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                viewModelStore = nc.viewModelStore;
            }
        }

        if (viewModelStore == null && custom == null) {
            return null;
        }

        //将ViewModel存储在 NonConfigurationInstances 对象中
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore;
        return nci;
    }
复制代码

于是我们知道,在屏幕旋转后,当新的 Activity 重新创建,并调用 ViewModelProviders.of(this).get(xxxModel.class) 时,又会在 getViewModelStore() 方法中获取老 Activity 保存的 ViewModelStore。

    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,
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                //👇从该对象中获取ViewModelStore
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
        return mViewModelStore;
    }
复制代码

而ViewModel实际是存储在ViewModelStore中的,ViewModelStore还原后,那么也就拿到了 ViewModel。具体代码如下所示: 从ViewModelStroe中获取ViewModel的相关代码

@NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            //noinspection unchecked
            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);
        //noinspection unchecked
        return (T) viewModel;
    }
复制代码

这就是为什么屏幕旋转后,ViewModel可以保存数据的原因

为什么Fragment中的数据屏幕旋转后可以保存?

如果我们在Fragment中调用如下代码:

    val model: MyViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
复制代码

追踪他们的调用,可以发现获取ViewModel是通过mNonConfig存储的

 @NonNull
    ViewModelStore getViewModelStore(@NonNull Fragment f) {
        return mNonConfig.getViewModelStore(f);
    }
复制代码

那么mNonConfig又是什么时候创建的呢?又存储在哪里?

 void attachController(@NonNull FragmentHostCallback<?> host,
            @NonNull FragmentContainer container, @Nullable final Fragment parent) {
        //省略更多...
        if (parent != null) {
            mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
        } else if (host instanceof ViewModelStoreOwner) {
            //👇走这里
            ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
            mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
        } else {
            mNonConfig = new FragmentManagerViewModel(false);
        }
    }
复制代码

在将Fragment添加到FragmentManager中时会调用到这 因为传入的 parent = null,且 Activity 默认实现了 ViewModelStoreOwner 接口,所以会获取 Activity 中的 ViewModelStore,接着调用 FragmentManagerViewModel 的 getInstance() 方法:

    static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
        ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
                FACTORY);
        return viewModelProvider.get(FragmentManagerViewModel.class);
    }
复制代码

在该方法中,会创建 FragmentManagerViewModel,并将其添加到 Activity 中的 ViewModelStore 中。

整体流程如下所示: ViewModel 在 Fragment 中不会因配置改变而销毁的原理

根据上面的分析,ViewModel 在 Fragment 中不会因配置改变而销毁的原因其实是因为其声明的 ViewModel 是存储在 FragmentManagerViewModel 中的,而 FragmentManagerViewModel 是存储在宿主 Activity 中的 ViewModelStore 中,又因 Activity 中 ViewModelStore不会因配置改变而销毁,故 Fragment 中 ViewModel 也不会因配置改变而销毁。

ViewModel 能在 Fragment 中共享的原理

ViewModel 的另一大特性就是能在 Fragment 中共享数据。

假如我们想 Fragment D 获取 Fragment A 中的数据,那么我们只有在 Activity 中的 ViewModelStore 下添加 ViewModel。只有这样,我们才能在不同 Fragment 中获取相同的数据。这也是为什么在 Fragment 中使用共享的 ViewModel 时,我们要在调用ViewModelProvider.of() 创建 ViewModel 时需要传入 getActivity() 的原因。

具体例子如下:

    public class SharedViewModel extends ViewModel {
        private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

        public void select(Item item) {
            selected.setValue(item);
        }

        public LiveData<Item> getSelected() {
            return selected;
        }
    }

    public class FragmentA extends Fragment {
        private SharedViewModel model;
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //👇传入的是宿主Activity
            model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
            itemSelector.setOnClickListener(item -> {
                model.select(item);
            });
        }
    }

    public class FragmentD extends Fragment {
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
             //👇传入的是宿主Activity
            SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
            model.getSelected().observe(this, { item ->
               // Update the UI.
            });
        }
    }
复制代码

参考资料

知识点 | ViewModel 四种集成方式
ViewModel 这些知识点你都知道吗?