为了账号安全,请及时绑定邮箱和手机立即绑定

Android 架构组件之 ViewModel

ViewModel 是用来保存应用 UI 数据的类,它会在配置变更后继续存在。本文将从以下几个方面来介绍 ViewModel。

  • 使用 ViewModel 有哪些优势?
  • 如何使用 ViewModel。
  • 分析 ViewModel 的组成及原理。
  • 总结 ViewModel 的使用及注意事项。

1. 使用 ViewModel 有哪些优势?

ViewModel 是用来保存应用 UI 数据的类,它会在配置变更后继续存在。让我们来看看它具有的特点。

手机屏幕旋转是配置变更的一种,当旋转屏幕时,Activity 会被重新创建,如果数据没有被正确的保存和恢复,就有可能丢失,从而导致明明奇妙的 UI 错误,甚至出现应用的崩溃,相反的,ViewModel 会在配置变更后继续存在。

Google 推荐的架构设计:
将应用所有的 UI 数据保存在 ViewModel 中,而不是 Activity 中,这样能确保数据不会收到配置变更带来的影响。

Google推荐架构.png_watermarklogo

Android 开发中,一个常见的坑,就是把很多变量,逻辑和数据放在 Activity 或 Fragment 中,这样的代码比较混乱和难以维护。这种开发模式也违反了单一责任的原则。

ViewModel 可以有效地划分责任。具体的,它可以用来保存 Activity 中的所有数据,然后 Activity 仅负责了解如何在屏幕上显示该数据和接收用户互动,但是它不会处理这些互动。

如果你的应用加载和存储数据,建议创建一个 Repository 的存储区类,另外,应该确保 ViewModel 不会因为承担过多的责任而变得臃肿。要避免这种情况,可以创建 Presenter 类,或者实现一种更成熟的架构。

了解了 ViewModel 在架构设计中的地位,再来看一下 ViewModel 的生命周期:

viewmodel-lifecycle.png_watermarklogo

从图中可以看出 ViewModel 在屏幕旋转后,ViewModel 继续存在,并没有出现重建。

2. 如何使用 ViewModel

要创建一个 ViewModel,首先需要扩展 ViewModel 类,然后将 Activity 中之前与 UI相关的实例变量,摆放在这个 ViewModel 中。

class ChronometerViewModel : ViewModel() {
    var mStartTime: Long = 0L
}

接着在 Activity 的 onCreate()方法中,从 ViewModel Provider 的框架实用类再获取 ViewModel,请注意:ViewModelProvider 将获取一个 Activity 实例,这种机制让你在旋转屏幕时,获取一个新的 Activity 实例,不过,请确保它始终与同一个 ViewModel 关联。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.chrono_activity_2)

        // The ViewModelStore provides a new ViewModel or one previously created.
        val chronometerViewModel = ViewModelProvider(this).get(ChronometerViewModel::class.java)

        if (chronometerViewModel.mStartTime == 0L) {
            // If the start date is not defined, it's a new ViewModel so set it.
            val startTime = SystemClock.elapsedRealtime()
            chronometerViewModel.mStartTime = startTime
            chronometer.base = startTime

        } else {
            // Otherwise the ViewModel has been retained, set the chronometer's base to the original
            // starting time.
            chronometer.base = chronometerViewModel.mStartTime ?: 0
        }
        chronometer.start()
    }

对于 ViewModel实例,你可以使用 getter()方法,从 Activity 直接获取 UI数据,ViewModel 的默认构造函数是没有任何参数的,如果想要修改,可以使用 ViewModelProvider.Factory 创造一个自定义函数。

2.1 通过 ViewModel 和 LiveData 实现 Fragment 之间数据共享

上面介绍的,是 ViewModel 最简单的用例。ViewModel 类也可以很友好地与 LiveData和 DataBinding互相搭配使用。使用 ViewModel 和 LiveData 可以创建反应式界面,也就是说当底层数据被更新时,UI 也会相应的自动更新。关于 LiveData 的介绍,查看Android 架构组件之 LiveData

通过 ViewModel 与LiveData的组合,实现 Fragment 之间数据的共享,先看一下示例运行的效果图。

fragment_share_data.gif_watermarklogo

界面中包含两个 Fragment,每一个 Fragment 中都有一个 SeekBar 控件,通过 ViewModel 与LiveData 实现 SeekBar 进度值的共享。

首先定义 ViewModel

class SeekBarViewModel : ViewModel() {
        val seekbarValue = MutableLiveData<Int>()
    }

在 Fragment 的 onCreateView()方法中,完成 viewModel 实例的获取,并完成注册监听。

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val root = inflater.inflate(R.layout.fragment_share_data, container, false)
        mSeekBar = root.findViewById(R.id.seekBar)
        mSeekBarViewModel = ViewModelProvider(requireActivity()).get(
            SeekBarViewModel::class.java
        )
        subscribeSeekBar()
        return root
    }

完整的示例代码,在文末给出 GitHub 地址。

3. 分析 ViewModel 的组成及原理

我们用官方最新的lifecycle-viewmodel:2.2.0-alpha02 版本来分析,先来看一张类图:

android_component_ViewModel_CD.jpg_water

从上面的类图我们可以看到:

  • ViewModelProviders 提供了多个 of() 重载的静态方法用来获取 ViewModelProvider 对象。

  • ViewModelProvider 中定义了一个接口 Factory 和两个内部类 NewInstanceFactory 和 AndroidViewModelFactory,NewInstanceFactory实现了 Factory 接口,AndroidViewModelFactory 是 NewInstanceFactory 的子类。Factory 接口中只有一个 create() 方法,接收一个实现了 ViewModel 类的 Class对象,返回该 Class对象 newInstance() 后的实例,可以看出 create()的实现都是通过反射完成。

  • 在 ViewModelProvider 中有两个成员变量 mFactory 和 mViewModelStore。mFactory 可以是通过 ViewModelProviders 的 of()传递进来的 Factory 对象,如果 of() 方法没有传进来 Factory 对象,会创建一个AndroidViewModelFactory对象。

  • 在 ViewModelStore 中维护着一个 Map 用来存储创建的 ViewModel 对象。

  • ViewModelProvider 的一个构造方法中需要一个实现了 ViewModelStoreOwner 接口的对象,最新的 androidx 包中的 Activity 和 Fragment 都实现了这个接口。这个接口只有一个 getViewModelStore()的方法,所以我们可以在 Activity 和 Fragment 中通过 getViewModelStore()方法获得 ViewModelStore 对象,通过 ViewModelStore维护的 Map 最终得到 ViewModel 实例。使用这种方法,我们可以实现 Fragment 之间的通信。

了解了跟 ViewModel 有关的类,我们再通过时序图来了解一下这些类之间的调用关系,在时序图中,我们重点关注几个过程:ViewModelStore 和 ViewModel 实例的创建过程,页面销毁时 onCleared()方法的执行过程。

ViewModel_SD.jpg_watermarklogo

3.1 ViewModelStore 和 ViewModel 实例的创建过程

首先我们在 Fragment(或Activity)中调用 ViewModelProviders 的 of() 方法,这个方法最终会返回一个 ViewModelProvider 对象。

在 of() 方法中主要完成了以下几个工作:

  • 确定 application,通过 checkApplication() 方法;
  • 确定 factory 对象,如果没有通过 of() 传入,调用ViewModelProvider 中的 AndroidViewModelFactory 创建;
  • 创建 ViewModelProvider 对象。
@NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {
    Application application = checkApplication(checkActivity(fragment));
        if (factory == null) {
            factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
        }
        return new ViewModelProvider(fragment.getViewModelStore(), factory);
    }

ViewModelStore 的创建过程,最新的 androidx 包中的 Activity 和 Fragment 都实现了 ViewModelStoreOwner 接口,这个接口只有一个 getViewModelStore() 的方法。在 Activity 和 Fragment 中通过 getViewModelStore() 方法获得 ViewModelStore 对象。而 ViewModelStore 中维护着的 Map 保存着 ViewModel 实例。

public interface ViewModelStoreOwner {

    @NonNull
    ViewModelStore getViewModelStore();
}

通过 of() 方法创建了 ViewModelProvider 对象后,再来看一下 ViewModelProvider 类中的 get() 方法:

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);
    }
    
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof OnRequeryFactory) {
                ((OnRequeryFactory) mFactory).onRequery(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;
    }
    
    private static final String DEFAULT_KEY =
            "androidx.lifecycle.ViewModelProvider.DefaultKey";

对上面的代码中进一步跟踪中,我们可以分析出:

  • ViewModel 实例的创建过程是通过反射完成的,获取的 ViewModel 实例保存在 ViewModelStore 中的一个 Map 成员变量中;

  • ViewModelStore 中 Map 的 Key 是 DEFAULT_KEY 与 ViewModel 完整包路径拼接而成;

  • 如果 Map 中包含了这个 ViewModel 的实例,直接返回。如果没有,则通过调用 factory 的 create() 方法,创建 ViewModel 实例,并将这个实例保存到 Map 中。

3.2 onCleared()方法的执行过程

分析过了 ViewModel 的创建过程,再来看看 ViewModel 的销毁过程。

在 ComponentActivity 中,通过注册一个匿名的 LifecycleEventObserver 对象,来监听界面的是否被销毁。当收到页面被销毁的通知后,会调用 ViewModelStore 中的 clear() 方法。

public ComponentActivity() {
    ....
    getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });
}

在 ViewModelStore 中,调用 Map 中每个 ViewModel 实例的 clear()方法,最后将 map 清空,至此,完成了 ViewModel 的销毁工作。

public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }

4. 总结 ViewModel 的使用及注意事项

最后说一些使用 ViewModel的注意事项:

首先,**任何时候,都不应该将 Context 传入 ViewModel。**也就是说,Fragment、Activity 和 View 都不能被传入,正如此前介绍的一样,ViewModel 可以比相联的 Activity 和 Fragment 的生命周期更长。假设在 ViewModel 中存储了一个 Activity,那么当你旋转屏幕时,那个 Activity 将被销毁,但是 ViewModel 还存储着已经被销毁的 Activity 的引用,这就是一种内存泄露。

如果你需要比 ViewModel 的生命周期更长的 Application 类,可以使用 AndroidViewModel 的子类,通过这个子类,就可以直接使用 Application 的引用了。

第二点,ViewModel 并不应该取代 onSaveInstanceState的使用,它们两个是相辅相成的,当进程被关闭时,ViewModel 将被销毁,但是 onSaveInstanceState 将不会受到影响。

另外,ViewModel 可以用来存储大量数据,而 onSaveInstanceState 就只可以用来存储有限的数据。我们尽可能把多一点的 UI数据往 ViewModel 内存储,以便在配置变更时不需要重新加载或生成数据,另一方面,如果进程被 Framework 关闭,我们应该用 onSaveInstanceState 来存储,足以还原 UI 状态的最少量数据。比如用户的数据库 ID。

更多内容,可以订阅 我的博客


参考文献

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
12
获赞与收藏
17

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消