LifeCycle、ViewModel、LiveData 的组合使用

LifeCycle、ViewModel、LiveData 的组合使用

前言

在文章正文开始之前,我们先引入几个问题:

在开发中,我们时常需要和Activity、Fragment 的生命周期打交道,它们本身也提供了各自一套生命周期的方法,我们只需要在相应的方法里面做正确的事情即可,但是我们有没有想过,这样对于 Activity 和 Fragment 来说,功能过于复杂了,那么如何将这一块业务逻辑从我们的视图之中迁移到业务层呢?

当我们开发的APP 需要适配键盘状态、屏幕旋转、语言变化等 configure 改变会导致 Activity销毁重建,这个时候 Activity 持有的数据就会全部丢失,在Activity 重建之后我们又要去重新获取数据,这期间可能会对用户体验造成不良影响,传统的解决办法是在 onSaveInstanceState方法中保存,在 onCreate中恢复,这样不仅会让 UI 层代码臃肿,而且存储的数据还必须是可序列化的。那么有没有办法将这部分逻辑从 UI 层中移除呢?

现在比较常用的设计架构有 MVP 和 MVVM,以 MVP 为例子,通用的做法是将业务逻辑抽成一个 Presenter 接口,将 UI 逻辑抽成一个 View 接口。在 Activity 中特定条件触发的时候调用特定的 Presenter 接口方法,在 PresenterImpl 实现类中某个特定的条件触发的时候调用 View 接口特定的方法,这样来完成业务逻辑层和 UI 层之间的交互,这种架构设计只适用PresenterImpl 和 View 接口之间每个方法存在一对一或者少量对一的调用关系的场景。我们假设一下Activity 中视图 A 的可见性依赖于 PresenterImpl 中的布尔值 isVisible属性,而 PresenterImpl中存在多处更改 isVisible 属性的情况,那么在这种情况下我们就需要在多个地方多次调用 View 接口中 showA()和 hiddenA()的操作了,这样的代码会让人不舒服,或者我们可以通过重写 isVisible 的设置器,但是这种无法得到保证,因为这要求程序员必须显式调用 isVisible 的设置器。那么在这种情况下我们如何才能实现在一处代码块中处理 isVisible 属性改变的调用,并且不依赖于程序员的自觉性呢?

当我们的 Activity、Fragment 不可见的时候,我们是不需要也不应该刷新 UI 的。比如开启一个倒计时任务,从10倒数到0,我们期望当我们的 Actvity、Fragment 不可见的 时候我们的计时器依然是有效的,但是不会去刷新 UI 的显示,在倒数过程中如果我们让我们的 Activity、Fragment 变得可见的时候,视图应该正确显示现在倒数到的最新数字,通过在设置视图显示文本的时候手动判断当前 Activity、Fragment 的生命周期可以达到我们的期望,但是手动是不可靠的,有没有自动的方式实现我们的期望呢?

解决的方法

解决问题一的方案是使用实现了 LifecycleOwner 的 Fragment、Activity,比如 V4包下的 Fragment、FragmentActivity、AppCompatActivity 之类的,lifecycleOwner 与传统的 Activity、fragment 不同之处是lifecycleOwner是一个接口,而不是一个实现类,这意味着任何类都可以实现这个接口,从而监听 Activity、Fragment 的生命周期变化

解决问题二的方案是使用 ViewModel 这个组件

解决问题三的方案是使用属性观察者,这里是使用 LiveData,其内部实现了有新值则回调通知外界的逻辑

解决问题四的方案依然是使用 LiveData,其实也就是注册监听属性变化的时候传入一个 LifecycleOwner,然后在回调数据改变方法之前判断当前的 Activity、Fragment 是否可见,如果不可见则不进行回调。在 Activity、Fragment 重新变得可见的时候会回调 LifecycleOwner 相应的方法,在这些方法里面让 LiveData 去检查视图最后接收到的值是否为最后一个改变的值,如果不是,则同步值到视图观察者即可。

三个组件的概要介绍

  • LifecycleOwner 是一个接口,它拥有 Activity、Fragment 所有生命周期的方法,在实现了这个接口的 Activity、Fragment 对象中,每个生命周期对应的方法都会被回调,LifecycleOwner 之所以设计成接口,是为了其它对象可以使用到,这样其它对象就无需要求 Activity、Fragment 在特定的生命周期中调用特定的方法,比如终结方法、暂停方法,而这些要求往往可能被程序员所忽略,也使 Activity、Fragment 变得臃肿复杂。

  • ViewModel ViewModel通常在Activity、Fragment 的生命周期内被创建并且与之关联在一起直到Activity 被 finish 掉。当 Activity、Fragment 被重新创建的时候,通过 ViewModelProvides 可以重新获得与之关联的 ViewModel,从而确保数据不会丢失。ViewModel 被存储在 ViewModelStore 类中,当与之关联的 Activity、Fragment 被 finish 之后最终会调用 ViewModel 的 onCleared 方法,到这里之后 ViewModel 才会被释放掉

  • LiveData 提供一个Observer接口给客户端,客户端实现这个接口,并将引用传递给它,当我们调用 LiveData 的 setValue 或 postValue 的时候,LiveData 就会去回调这个接口的方法,其实也就是LiveData 自己实现了观察者模式。不过 LiveData 搞定了一个让我们长期以来头疼的事情,那就是当 Activity、Fragment 不可见的时候 UI 不要更新的问题,其内部其实也是监听了 LifecycleOwner 的可见和不可见的方法,当 Activity、Fragment 不可见的时候就不回调 Observer 的方法,当 Activity、Fragment 变成可见的时候,就对比最新的值和最后发出去的值是否是同一个,不同的话就发送最新的值,确保 Activity、Fragment 可见的时候用户看到的一定是最新的数据。

上面的介绍就是这三个组件最核心的功能,虽然看上去没有什么特别难的地方,但是确实让我们的 Activity、Fragment 变得更加纯粹,也让我们的业务逻辑变得更加内聚、不依赖客户端程序员。更少的依赖、更集中的代码会让我们的程序变得更加稳固、易读,也让我们的程序维护起来更加便利。

在详细介绍这三个组件之前,我们先上源码,这份源码来自google 的 codelabs,github 地址

LifecycleOwner 简单使用

其实这个LifecycleOwner真的没什么好说的,因为实在太简单了,我觉得通过上面的说明之后,我们应该是知道这个是什么东西,实现原理是什么。
需要注意的就是只有支持包V4下面的 Fragment、FragmentActivity、AppCompatActivity等等几个类是实现了 LifecycleOwner 接口的。目前我们使用的 AS 只要是3.0.0以上的,新建的项目中 MainActivity 应该都是继承自AppCompatActivity的。
举个例子,我们以定位为例子,可以参考BoundLocationManager。以往我们需要在 onResume 和 onPause 中分别启动和暂停定位器,现在我们可以将其从 Activity、Fragment 中移除,代码如下

static class BoundLocationListener implements LifecycleObserver {
    private final Context mContext;
    private LocationManager mLocationManager;
    private final LocationListener mListener;

    public BoundLocationListener(LifecycleOwner lifecycleOwner,
                                 LocationListener listener, Context context) {
        mContext = context;
        mListener = listener;
        lifecycleOwner.getLifecycle().addObserver(this);
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    void addLocationListener() {

        Location lastLocation = //获取用户定位
        if (lastLocation != null) {
            mListener.onLocationChanged(lastLocation);
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    void removeLocationListener() {
        if (mLocationManager == null) {
            return;
        }
        mLocationManager.removeUpdates(mListener);
        mLocationManager = null;
    }
}

通过@OnLifecycleEvent标注我们可以标注我们的方法所对应的生命周期阶段,当然我们也可以直接实现GenericLifecycleObserver接口,在void onStateChanged(LifecycleOwner source, Lifecycle.Event event);中判断 Event 的值,不过通过标注会让代码更加纯粹易读。

ViewModel 简单剖析

如上面的简介,ViewModel 中保存的数据可以躲避因 configure 发生改变而丢失的问题,我们先看下如何使用 ViewModel
如工程中的ChronometerViewModel类所示,只需要继承自 ViewModel 类即可,代码如下

public class ChronometerViewModel extends ViewModel {

    @Nullable
    private Long mStartTime;

    @Nullable
    public Long getStartTime() {
        return mStartTime;
    }

    public void setStartTime(final long startTime) {
        this.mStartTime = startTime;
    }
}

当然没有这么简单,重要的是如何获取这个 ViewModel 的对象,我们在ChronoActivity2onCreate中通过以下的代码获取 ViewModel 对象

ChronometerViewModel chronometerViewModel = ViewModelProviders.of(this).get(ChronometerViewModel.class);

至于是创建还是重新连接已经存在的 ViewModel,就靠 ViewModelProviders 与其相关的系列工厂方法去判断,我们无需关心
当 Activity 被销毁重建之后,系统会补偿我们一个savedInstanceState对象,在 Activity 中通过这个对象系统可以恢复FragmentManager,这个 FragmentManager主要就是用来寻找HolderFragment这个对象的,通过这个对象我们就可以找到之前Activity、Fragment 关联的 ViewModelStore,进而找到对应的 ViewModel,具体分析如下:

  1. ViewModelProviders.of(this)通过静态工厂方法创建一个全新的对象ViewModelProvider对象,但是这个对象里面的mViewModelStore属性却不一定是全新的,通过跟踪代码,我们可以发现在HolderFragment类的内部类HolderFragmentManager中有如下属性
private Map<Activity, HolderFragment> mNotCommittedActivityHolders = new HashMap<>();
private Map<Fragment, HolderFragment> mNotCommittedFragmentHolders = new HashMap<>();

即对于一个 Activity、Fragment 的实例或者是销毁重建后的新实例,都只会返回相同的一个HolderFragment实例,可以通过下面的方法证明

HolderFragment holderFragmentFor(FragmentActivity activity) {
    //这行代码获取的的 fm 就是我们上文所说的系统补偿给意外重建的 Activity 的参数中获取的信息恢复出来的 fm
    FragmentManager fm = activity.getSupportFragmentManager();
    //找到了 holder,我们就可以找到 ViewModelStore,进而找到 ViewModel
    HolderFragment holder = findHolderFragment(fm); 

    //获取 map 中 activity key 对应的值
    holder = mNotCommittedActivityHolders.get(activity);
    if (holder != null) {
        return holder;
    }

    holder = createHolderFragment(fm);
    //将新创建的对象作为value,activity 作为 key 保存到 map 中
    mNotCommittedActivityHolders.put(activity, holder);
    return holder;
}
  1. 我们接下去看ChronometerViewModel chronometerViewModel = provider.get(ChronometerViewModel.class);这个方法的实现,通过跟进代码,我们确实发现我们的 ViewModel 对象是在mViewModelStore单例属性中获取的,代码如下
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    //获取 ViewModel 对象
    ViewModel viewModel = mViewModelStore.get(key);
    if (modelClass.isInstance(viewModel)) {
        return (T) viewModel;
    }
    //如果不存在则创建一个新的 ViewModel,并且缓存起来
    viewModel = mFactory.create(modelClass);
    mViewModelStore.put(key, viewModel);
    return (T) viewModel;
}

通过上面的代码,我们也就可以知道ViewModel 相关工厂类的内部逻辑了,简单说起来就是 viewModel 被缓存起来了,只要被关联的 key 没有变,那么就可以取到与之关联的 ViewModel 对象,也就是我们前言所说的如何将这一块判断逻辑从 Activity、Fragment 移除出去,通过 ViewModel 相关工厂方法就可以实现了。
通过上面的分析我们知道这一切的关键在于ViewModelProvider对象的一个单例字段 mViewModelStore,严格意义上来说,这个不是单例,他会随着 Activity、Fragment 的销毁而销毁,因此也不会造成内存泄露。当然客户端程序员不需要显式地调用其终结方法,因为HolderFragmentManager内部会监听 LifecycleOwner 的销毁方法,在其中移除对 ViewModel 对象的引用。

需要注意的是,正如上段代码所示ViewModel viewModel = mViewModelStore.get(key);,mViewModelStore是通过一个字符串去获取 ViewModel的,而这个 key 的值相对具体的 ViewModel 类是不会变化的,所以就算我们在 Activity 中多次调用

ChronometerViewModel chronometerViewModel = ViewModelProviders.of(this).get(ChronometerViewModel.class);

获取到的对象都是同一个对象,所以假设我们的 Activity 中拥有多个 Fragment,每个 Fragment 都要获取到相同的 ViewModel对象,那么这个时候可以这样写

//传递 getActivity(),而不是 this
ChronometerViewModel chronometerViewModel = ViewModelProviders.of(getActivity()).get(ChronometerViewModel.class);

因为对于不同的 Fragment,它们唯一的关联就是属于同一个 Activity,如果传递的是 this,那么获取到的 ViewModel 就不是同一个对象了,这一点在源码 Fragment_step5中有所体现。

注意,由于 ViewModel 的生命周期会比 Activity 长,所以一定不要在 ViewModel 中持有 Activity 的引用,以免造成内存泄露

LiveData 简单剖析

LiveData感觉也没什么好说的,我们先看一下用法,在 Activity、Fragment 中我们观察 LiveData 的变化,可以看ChronoActivity3代码如下

private void subscribe() {
    final Observer<Long> elapsedTimeObserver = new Observer<Long>() {
        @Override
        public void onChanged(@Nullable final Long aLong) {
            String newText = ChronoActivity3.this.getResources().getString(
                R.string.seconds, aLong);
            ((TextView) findViewById(R.id.timer_textview)).setText(newText);
            Log.d("ChronoActivity3", "Updating timer");
        }
    };

    mLiveDataTimerViewModel.getElapsedTime().observe(this, elapsedTimeObserver);
}

其实就是把 lifecycleOwner 和一个callBack 类传递进去,我们知道 lifecyclOwner 是为了让 LiveData 内部判断是否要通知 Activity、Fragment 有新值变化了。
接下去我们看看 LiveData 怎么创建和传递值的,可以LiveDataTimerViewModel,代码如下

public class LiveDataTimerViewModel extends ViewModel {

    private static final int ONE_SECOND = 1000;

    private MutableLiveData<Long> mElapsedTime = new MutableLiveData<>();

    private long mInitialTime;

    public LiveDataTimerViewModel() {
        mInitialTime = SystemClock.elapsedRealtime();
        Timer timer = new Timer();

        // Update the elapsed time every second.
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                final long newValue = (SystemClock.elapsedRealtime() - mInitialTime) / 1000;
                // setValue() cannot be called from a background thread so post to main thread.
                mElapsedTime.postValue(newValue);
            }
        }, ONE_SECOND, ONE_SECOND);

    }

    public LiveData<Long> getElapsedTime() {
        return mElapsedTime;
    }
}

其实就是创建一个 MutableLiveData 对象,然后在需要传递值的时候调用 postValue 就可以了。具体的 LiveData 实现并不复杂,感兴趣的朋友可以自行查看。
这里我们就只说几点使用 LiveData 需要注意的事情:

  • LiveData 是一个抽象类,我们无法直接创建,只能使用它的实现类 MutableLiveData来创建对象,但是我们对外声明还是要用 LiveData,因为它的 postValue、setValue等是 protected 访问权限的,这样做之后外界就无法修改我们的值,不要给外部过大的权限。
  • setValue 需要在主线程中调用,如果在子线程中,那么需要使用 postValue。
  • liveData 可以在任意线程被订阅,但观察者接收到变化数据的 onChanged 方法一定是在主线程中被执行,因为这是由setValue 和 postValue 内部调用 onChanged 方法所在的线程决定的,而这两个方法的实现都是在主线程。

三个组件组合使用

通过上面小结的内容之后,我们知道 LifecyeleOwner 和 ViewModel、LiveData 之间都是有紧密联系的,这一点无需我们通过代码去组合,我们唯一能够组合的就是 LiveData 和 ViewModel,两者结合在一起之后其实也没有什么生成什么化学反应,也就是同时拥有 ViewModel 和 LiveData 的特性,所以也没有什么好举例的,只要了解了这三个组件的原理之后,想怎么组合使用都不是什么问题。

通过组合这三个组件之后我们现在的程序就变得职责分明了,具体有以下几点体现

  • Activity、Fragment 更加纯粹,只需要用来管理UI 逻辑,不需要了解业务层与生命周期之间的关系
  • 业务层逻辑更加内聚,无需依赖 UI 去做生命周期相关阶段的处理,避免出错
  • 无需在 onCreate 中判断是否要创建新业务数据还是从缓存中获取,让 UI 层代码更加简介
  • 当一个信号量可能在多个地方改变的时候,使用 LiveData 可以只在一处代码块中做逻辑处理,而不需要分散代码或者重写 setter 方法并强制程序员调用 setter 方法,当然当某个 UI的状态依赖的是多个信号量或者依赖的信号量是由多个信号量组成的,那么这个时候用 RxJava2会让代码更加纯粹、集中、易读。

给我感觉最强大的是 LifecycleOwner,因为现在我们可以直接在我们的业务类中监听 Activity、Fragment 的变化,而不需要向外接再三强调一定要在 onResume 中调用开始,在 onPause中调用暂停,在 onDestory 中终结,这样的代码只需要在业务类中一次实现,就可以处处使用。

至于说什么时候使用这三个组件,这里我简单说一下我的看法,首先 LifecycleOwner 和 ViewModel 是一定要处处用到的,具体原因上面也说了,虽然人家叫做 ViewModel,但是对于我们选择用 MVP 或者是 MVVM 并没有什么冲突,因为ViewModel 的概念和这两种架构没有冲突,所以剩下的就是 LiveData 了,LiveData 的使用场景适合在一个信号量在多处改变的时候使用,当一个信号量只在一个地方改变,那么用不用 LiveData 其实差不不大。所以我的定位是一个信号量在多处地方改变的时候

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容