Android开发之MVVM模式实践(一):ViewModel的封装

前言

本文发布于去年下半年,在发布两篇系列文章后未再更新后续系列。期间虽然收到多位读者催更,但因工作原因以及去年自我学习驱动在Python上,未再做更新。在此感谢各位同学的关注同时对断更表示抱歉。本系列文章将在今后两周内正常更新完毕,同时将重新更新包括本篇在内已经发布的系列文章,提升阅读体验。本系列文章涉及知识点主要为ViewModelLiveDataDataBinding以及Kotlin的协程,欢迎各位同学关注。

推荐

文章将率先在公众号「Code满满」与个人博客「李益的小站」上发布,欢迎大家关注!

架构境况

目前Android开发中,常用的几种项目架构模式分别是MVCMVPMVVM。当然根据项目的体量和业务的不同,可能还会对这几种模式进行融合,产生其他变种模式,这个我们暂且不谈。我们本篇的主角是近两年越来越受欢迎的MVVM,主要依托于Google的推出的Jetpack全家桶实现。

一、初识ViewModel

ViewModelJetpack全家桶中的一员,也是构建MVVM模式的重要组成部分。因为ViewModel拥有比Activity/Fragment还要长的生命周期,所以ViewModel中最好不要持有Activity/Fragment的引用,以免引起内存泄漏。一般建议在ViewModel只做数据处理,保存Activity/Fragment中的页面数据,在Activity被销毁重建时也能拿到之前的页面数据。此外,Google建议一个Activity/Fragment最好只拥有一个ViewModel,一个ViewModel中可以拥有多个Model实例(即数据逻辑处理的类,比如网络请求数据)。

eg: 当屏幕旋转时,Activity可能会先被销毁,再重新创建新的实例;而因为ViewModel的生命周期长于Activity,新的Activity中的ViewModel持有的是之前被销毁的Activity的引用(具体原理可看ViewModelProvider源码中对ViewModel的存取处理),这样就会导致内存泄漏。

二、给ViewModel添加页面的生命周期函数

在开发中,ViewModel中的方法不可避免的会被在指定的Activity/Fragment的生命周期函数中调用,我们可以在Activity/Fragment的生命周期函数中主动调用ViewModel中的方法,但是我们还有一种更完美的方法,即让ViewModel拥有和Activity/Fragment一样的生命周期函数,ViewModel在自己的生命周期函数中主动调用自己的方法即可,与Activity/Fragment更加解耦,也更加方便单元测试。

常见的生命周期实现方法

ViewModel拥有和Activity一样的生命周期函数,我们常见的做法是在Activity基类中的生命周期函数中调用ViewModel基类中对应的生命周期函数,达到ViewModel的生命周期函数和Activity的生命周期函数同步的效果。

class BaseActivity:AppCompatActivity(){
    ......
    override fun onResume() {
        super.onResume()
        viewModel.onResume()
    }
}

class BaseViewModel:ViewModel(){
    fun onResume(){
    }
}

LifecycleObserver的使用

虽然上面的常见方法已经可以满足我们的基本需求,但是Google的Jetpack还给了我们一种更加完美的实现:LifecycleObserverLifecycleObserver是一个接口,表示组件生命周期的观察者,使用如下:

interface ViewModelLifecycle:LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
    fun onAny(owner: LifecycleOwner, event: Lifecycle.Event)

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreate()

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onStart()

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume()

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause()

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun onStop()

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy()
}

上述代码中,我们创建了一个名为ViewModelLifecycle的接口,并继承于LifecycleObserver。然后在ViewModelLifecycle中定义了onCreateonResume等诸多方法,并在方法上加上了与生命周期函数对应的注解,表示Activity在执行自己的生命周期函数时,也会调用对应注解修饰的自定义函数(注解中的Lifecycle.Event.ON_ANY表示只要Activity执行自己的生命周期函数,此注解修饰的方法就会被调用)。

关于Lifecycle相关的知识本篇内容不会详细讲解,有兴趣的同学可以自行查询相关资料

为ViewModel添加生命周期函数

在定义完接口后,我们下一步就需要ViewModel去实现上述的ViewModelLifecycle接口,为自己添加生命周期函数,实现如下:

abstract class BaseViewModel : ViewModel(), ViewModelLifecycle {
    private lateinit var lifcycleOwner: LifecycleOwner

    override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
        this.lifcycleOwner = owner
    }

    override fun onCreate() {

    }

    override fun onStart() {

    }

    override fun onResume() {

    }

    override fun onPause() {

    }

    override fun onStop() {

    }

    override fun onDestroy() {

    }
}

至此,ViewModel就拥有了自己的生命周期函数了,但是这只是半成品。在上述内容中提到LifecycleObserver表示的是组件生命周期的观察者,既然有观察者,那么一定就有被观察过者被观察者需要实现的接口是LifecycleOwner。而作为被观察的对象Activity/Frament则需要实现LifecycleOwner。不过在AndroidX中的Activity/Frament已经默认实现了LifecycleOwner接口,我们只需要在Activity/Fragment中进行如下操作即可使ViewModel的生命周期函数与Activity/Fragment的生命周期函数同步:

// 将实现了LifecycleObserver接口的ViewModol实例作为观察者,添加到Activity/Fragment的生命周期观察者队列中
// 可以在实例化ViewModel后,即刻调用这个方法
getLifecycle().addObserver(viewModel)

// 将ViewModel从Activity/Fragment的生命周期观察者队列中移除
// 一般可以在onDestory()方法中调用这个方法
getLifecycle().removeObserver(viewModel)

三、为ViewModel添加一些常用事件

我们在开发中经常会遇到这样的一些场景:

  • 网络请求数据时,发生错误,弹出toast提示
  • 网络请求数据后,数据源为空,显示无数据视图
  • 网络请求数据时,弹出Loading视图,在网络请求结束后关闭
  • ......

对于上述提到的类似操作,我们可以事先在ViewModel中埋下用于通信使用的对应的LiveData,利用LiveData的观察者机制和在页面处于非活跃状态下不会通知UI更新的特性,便捷的通知Activity/Fragment作出对应的UI处理,并且完美的避过内存泄漏。

  1. 定义常用的UI操作
interface ViewBehavior {
    /**
     * 是否显示Loading视图
     */
    fun showLoadingUI(isShow: Boolean)

    /**
     * 是否显示空白视图
     */
    fun showEmptyUI(isShow: Boolean)

    /**
     * 弹出Toast提示
     */
    fun showToast(map: Map<String, *>)

    /**
     * 不带参数的页面跳转
     */
    fun navigate(page: Any)

    /**
     * 返回键点击
     */
    fun backPress(arg: Any?);

    /**
     * 关闭页面
     */
    fun finishPage(arg: Any?)
}
  1. ViewModel中添加事件LiveData
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {
    // loading视图显示Event
    var _loadingEvent = MutableLiveData<Boolean>()
        private set

    // 无数据视图显示Event
    var _emptyPageEvent = MutableLiveData<Boolean>()
        private set

    // toast提示Event
    var _toastEvent = MutableLiveData<Map<String, *>>()
        private set

    // 不带参数的页面跳转Event
    var _pageNavigationEvent = MutableLiveData<Any>()
        private set

    // 点击系统返回键Event
    var _backPressEvent = MutableLiveData<Any?>()
        private set

    // 关闭页面Event
    var _finishPageEvent = MutableLiveData<Any?>()
        private set

    lateinit var application: Application

    private lateinit var lifcycleOwner: LifecycleOwner

    override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
        this.lifcycleOwner = owner
    }

    override fun onCreate() {

    }

    override fun onStart() {

    }

    override fun onResume() {

    }

    override fun onPause() {

    }

    override fun onStop() {

    }

    override fun onDestroy() {

    }

    override fun showLoadingUI(isShow: Boolean) {
        _loadingEvent.postValue(isShow)
    }

    override fun showEmptyUI(isShow: Boolean) {
        _emptyPageEvent.postValue(isShow)
    }

    override fun showToast(map: Map<String, *>) {
        _toastEvent.postValue(map)
    }

    override fun navigate(page: Any) {
        _pageNavigationEvent.postValue(page)
    }

    override fun backPress(arg: Any?) {
        _backPressEvent.postValue(arg)
    }

    override fun finishPage(arg: Any?) {
        _finishPageEvent.postValue(arg)
    }

    protected fun showToast(str: String) {
        showToast(str, null)
    }

    protected fun showToast(str: String, duration: Int?) {
        val map = HashMap<String, Any>().apply {
            put(
                FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
                FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_STR
            )
            put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, str)
            if (duration != null) {
                put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
            }
        }
        showToast(map)
    }

    protected fun showToast(@StringRes resId: Int) {
        showToast(resId, null)
    }

    protected fun showToast(@StringRes resId: Int, duration: Int?) {
        val map = HashMap<String, Any>().apply {
            put(
                FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
                FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_RESID
            )
            put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, resId)
            if (duration != null) {
                put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
            }
        }
        showToast(map)
    }

    protected fun backPress() {
        backPress(null)
    }

    protected fun finishPage() {
        finishPage(null)
    }
}
  1. Activity中对ViewModel中的通知事件进行处理
abstract class BaseBVMActivity<B : ViewDataBinding, VM : BaseViewModel> : BaseBindingActivity<B>(),
    ViewBehavior {

    protected lateinit var viewModel: VM

    protected fun injectViewModel() {
        val vm = createViewModel()
        viewModel = ViewModelProvider(this, BaseViewModel.createViewModelFactory(vm))
            .get(vm::class.java)
        viewModel.application = application
        lifecycle.addObserver(viewModel)
    }

    override fun init(savedInstanceState: Bundle?) {
        injectViewModel()
        initialize(savedInstanceState)
        initInternalObserver()
    }

    fun getActivityViewModel(): VM {
        return viewModel
    }

    override fun onDestroy() {
        super.onDestroy()
        binding.unbind()
        lifecycle.removeObserver(viewModel)
    }

    protected fun initInternalObserver() {
        viewModel._loadingEvent.observeNonNull(this, {
            showLoadingUI(it)
        })
        viewModel._emptyPageEvent.observeNonNull(this, {
            showEmptyUI(it)
        })
        viewModel._toastEvent.observeNonNull(this, {
            showToast(it)
        })
        viewModel._pageNavigationEvent.observeNonNull(this, {
            navigate(it)
        })
        viewModel._backPressEvent.observeNullable(this, {
            backPress(it)
        })
        viewModel._finishPageEvent.observeNullable(this, {
            finishPage(it)
        })
    }

    protected abstract fun createViewModel(): VM

    /**
     *  初始化操作
     */
    protected abstract fun initialize(savedInstanceState: Bundle?)
}

四、为ViewModel添加Application

ViewModel中,我们可能会使用到Context,但是在文章开始时就提到,在ViewModel中并不适合持有Activity/Fragment的引用。所以Google为我们提供了一个AndroidViewModel,源码很简单,如下:

public class AndroidViewModel extends ViewModel {
    @SuppressLint("StaticFieldLeak")
    private Application mApplication;

    public AndroidViewModel(@NonNull Application application) {
        mApplication = application;
    }

    /**
     * Return the application.
     */
    @SuppressWarnings("TypeParameterUnusedInFormals")
    @NonNull
    public <T extends Application> T getApplication() {
        //noinspection unchecked
        return (T) mApplication;
    }
}

上面的AndroidViewModel仅仅是在ViewMdoel中添加了一个Application的引用,我们完全可以自己实现这一步,让我们的封装更灵活。

abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {
    ......

    @SuppressLint("StaticFieldLeak")
    lateinit var application: Application

    ......
}

abstract class BaseBVMActivity<B : ViewDataBinding, VM : BaseViewModel> : BaseBindingActivity<B>(),
    ViewBehavior {

    protected lateinit var viewModel: VM

    protected fun injectViewModel() {
        val vm = createViewModel()
        viewModel = ViewModelProvider(this, BaseViewModel.createViewModelFactory(vm))
            .get(vm::class.java)
        viewModel.application = application
        lifecycle.addObserver(viewModel)
    }
}

五、使用工厂模式创建ViewModel

ViewModel的创建并不止一种形式,我们常用的创建方式,如下

 val viewModel = ViewModelProvider(this).get(vm::class.java)

使用以上的方式创建ViewModel基本可以满足我们一般的需求,但是假如我们需要在实例化ViewModel的时候传入参数,那么我们就必须使用工厂方式来创建ViewModel,示例如下:

val vm = LoginViewModel(loginRepository)
viewModel = ViewModelProvider(this,BaseViewModel.createViewModelFactory(vm)).get(vm:class.java)

六、小结

本章对于ViewModel的封装暂时告一段落,下一章我们将主要讲解对Activity的封装。完整的封装代码已上传至Github,项目地址为:https://github.com/albert-lii/Fly-Android。最后附上完整的ViewModel封装代码:

interface ViewModelLifecycle:LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
    fun onAny(owner: LifecycleOwner, event: Lifecycle.Event)

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreate()

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onStart()

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume()

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause()

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun onStop()

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy()
}

/**
 * @author: Albert Li
 * @contact: albertlii@163.com
 * @time: 2020/6/9 8:01 PM
 * @description: 页面的常用操作
 * @since: 1.0.0
 */
interface ViewBehavior {
    /**
     * 是否显示Loading视图
     */
    fun showLoadingUI(isShow: Boolean)

    /**
     * 是否显示空白视图
     */
    fun showEmptyUI(isShow: Boolean)

    /**
     * 弹出Toast提示
     */
    fun showToast(map: Map<String, *>)

    /**
     * 不带参数的页面跳转
     */
    fun navigate(page: Any)

    /**
     * 返回键点击
     */
    fun backPress(arg: Any?);

    /**
     * 关闭页面
     */
    fun finishPage(arg: Any?)
}

/**
 * @author: Albert Li
 * @contact: albertlii@163.com
 * @time: 2020/6/7 10:30 PM
 * @description: ViewModel的基类
 * @since: 1.0.0
 */
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {

    // loading视图显示Event
    var _loadingEvent = MutableLiveData<Boolean>()
        private set

    // 无数据视图显示Event
    var _emptyPageEvent = MutableLiveData<Boolean>()
        private set

    // toast提示Event
    var _toastEvent = MutableLiveData<Map<String, *>>()
        private set

    // 不带参数的页面跳转Event
    var _pageNavigationEvent = MutableLiveData<Any>()
        private set

    // 点击系统返回键Event
    var _backPressEvent = MutableLiveData<Any?>()
        private set

    // 关闭页面Event
    var _finishPageEvent = MutableLiveData<Any?>()
        private set

    @SuppressLint("StaticFieldLeak")
    lateinit var application: Application

    private lateinit var lifcycleOwner: LifecycleOwner

    override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
        this.lifcycleOwner = owner
    }

    override fun onCreate() {

    }

    override fun onStart() {

    }

    override fun onResume() {

    }

    override fun onPause() {

    }

    override fun onStop() {

    }

    override fun onDestroy() {

    }

    override fun showLoadingUI(isShow: Boolean) {
        _loadingEvent.postValue(isShow)
    }

    override fun showEmptyUI(isShow: Boolean) {
        _emptyPageEvent.postValue(isShow)
    }

    override fun showToast(map: Map<String, *>) {
        _toastEvent.postValue(map)
    }

    override fun navigate(page: Any) {
        _pageNavigationEvent.postValue(page)
    }

    override fun backPress(arg: Any?) {
        _backPressEvent.postValue(arg)
    }

    override fun finishPage(arg: Any?) {
        _finishPageEvent.postValue(arg)
    }

    protected fun showToast(str: String) {
        showToast(str, null)
    }

    protected fun showToast(str: String, duration: Int?) {
        val map = HashMap<String, Any>().apply {
            put(
                FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
                FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_STR
            )
            put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, str)
            if (duration != null) {
                put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
            }
        }
        showToast(map)
    }

    protected fun showToast(@StringRes resId: Int) {
        showToast(resId, null)
    }

    protected fun showToast(@StringRes resId: Int, duration: Int?) {
        val map = HashMap<String, Any>().apply {
            put(
                FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
                FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_RESID
            )
            put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, resId)
            if (duration != null) {
                put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
            }
        }
        showToast(map)
    }

    protected fun backPress() {
        backPress(null)
    }

    protected fun finishPage() {
        finishPage(null)
    }

    companion object {

        @JvmStatic
        fun <T : BaseViewModel> createViewModelFactory(viewModel: T): ViewModelProvider.Factory {
            return ViewModelFactory(viewModel)
        }
    }
}


/**
 * 创建ViewModel的工厂,以此方法创建的ViewModel,可在构造函数中传参
 */
class ViewModelFactory(val viewModel: BaseViewModel) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return viewModel as T
    }
}