丢掉EventBus,ViewModel+LiveData用起来

需求背景

ViewModel和LiveData是google官方架构JetPack系列的一个响应式开发框架。ViewModel和LiveData主要用于搭建MVVM架构,能监听组件的生命周期变化,这样一来只会更新处于活跃状态的组件。
在我们工程中,ViewModel和LiveData的使用已经有一段时间,结合这两者的封装,我们在推动项目从MVP到MVVM的过渡。而在之前的过渡过程中,我们还只是用ViewModel来代替Presenter,用LiveData来代替Callback回调。
但最近的项目开发中,遇到了这样一个需求:一个activity的viewpager中有2个Fragment,其中第一个FragmentA支持上下滑动,每次滑动之后要通知到FragmentB刷新数据,这是个类似抖音的交互过程。其实在之前的开发中,我们就遇到过类似的需求,说白了就是一个页面甚至跨页面的多个UI组件的数据联动,这种场景相信在大家的日常开发中也经常遇到。比如在页面的弹框里给影片点击“想看”,那么这个页面甚至所有的“想看”状态都要刷新。而之前的做法无非两种:

  1. 使用回调,将操作回调给Fragment或者activity,再下发给需要更新的组件。这种做法如果只有一两层的回调还好,如果层级多了可能陷入“回调地狱”。 如果是多层次的UI,甚至有用到RecyclerView,是从ViewHolder里发出事件,穿越一层层去到另一个ViewHolder,那就几乎会破坏每一层的封装结构;
  2. 使用EventBus类似机制,或者自己实现的静态全局回调;这种机制通过一种类似全局总线的机制来解决问题。这种观察者模式需要自己去维护监听与移除,如果注册或者更新时机不对有可能造成内存泄漏甚至崩溃;但是EventBus或者类似的机制也存在滥用的痛点,一旦工程中使用太多,有可能造成event满天飞,到处都是收发的地方,给后期的维护带来困难;

用法与原理简介

现在我们有了ViewModel+LiveData,多了一种新思路来解决这个问题,这里首先要介绍一下ViewModel的用法,一个典型的ViewModel的使用示例:

//构建ViewModel实例
final FilmViewModel filmViewModel = ViewModelProviders.of(this).get(FilmViewModel class);

//观察ViewModel中数据的变化,并实时展示
filmViewModel.getFilmInfo().observe(this, new Observer<FilmInfo>() {
    @Override
    public void onChanged(FilmInfo filmInfo) {
        // reload
    }
});

findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        //点击按钮  更新Film
        filmViewModel.switchFilm();
    }
});

上面的示例可以看到,viewmode的对象不是new出来的,而是通过ViewModelProviders的get方法get出来的,下面看一下两个方法的内部实现:

@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
        @Nullable Factory factory) {
    //检查application是否为空,不为空则接收
    Application application = checkApplication(activity);
    if (factory == null) {
        //构建一个ViewModelProvider.AndroidViewModelFactory
        factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }
    return new ViewModelProvider(activity.getViewModelStore(), factory);
}

@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(ViewModelStores.of(fragment), factory);
}

ViewModelProvider只是个包装类,真正的viewmode缓存是在ViewModelSotre中,而ViewModelStore的获取方法实现如下:

    @NonNull
    @MainThread
    public static ViewModelStore of(@NonNull Fragment fragment) {
        if (fragment instanceof ViewModelStoreOwner) {
            return ((ViewModelStoreOwner) fragment).getViewModelStore();
        }
        return holderFragmentFor(fragment).getViewModelStore();
    }

只要是support包26.1.0之后的Fragment和FragmentActivity,都实现了ViewModelStoreOwner接口,而实现的方式也很简单,就是持有了一个ViewModelStore,那这个东西又是什么呢?看下源码就知道:

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.onCleared();
        }
        mMap.clear();
    }
}

其实没有什么特别的,就可以理解成一个HashMap的封装,而我们的ViewModel会用类名等参数做为主key被存储在这里。所以挖了大概三层方法,看似很长,结论其实很简单,就是在ViewModelProviders.of(a).getViewModel(XXXViewModel.class) 这样一个调用中,如果你传入的a是一样的,获取同样的XXXViewModel永远是同一个对象,除非activity或者fragment被onDestroy销毁重建;

解决问题

利用这样的特性,我们就可以用来作为同一个activity或者同一个fragment中的通信,比如以下这些场景:

  1. 同一个Fragment上不同UI组件的通信,都通过当前fragment去获取viewmodel,然后操作viewmodel中的数据即可做到一处更改,处处更新;
  2. 同一个activity上fragment之间的通信,都通过当前activity去获取viewmodel,然后操作viewmodel中的数据;
    这种用法,既避免了一层层的回调地狱,也不用定义一个个event,只需要修改数据。而且最重要的是,这一切建立在ViewModel+LiveData基础上,意味着都是对组件的生命周期敏感的,作为开发者我们一不用考虑各种各样的register和unregister场景,避免内存泄漏,也不用管因为生命周期引起的销毁重建导致的数据不一致问题。
    贴一段网上关于LiveData的特点介绍可以帮助理解:

LiveData的特点:
1)采用观察者模式,数据发生改变,可以自动回调(比如更新UI)。
2)不需要手动处理生命周期,不会因为Activity的销毁重建而丢失数据。
3)不会出现内存泄漏。
4)不需要手动取消订阅,Activity在非活跃状态下(pause、stop、destroy之后)不会收到数据更新信息。

深层思考

这里要更深挖一层,对于ViewModel+LiveData的合理使用有一个更深的理解。以往的使用中,我们将ViewModel作为MVP下Presenter的替代,只是将逻辑换了一种写法,但其实ViewModel的封装应该更进一步。比如之前的MVP模式下,P层很少会将数据缓存下来,而是View层调用P层接口,P层通过M层完成数据获取或更新后,直接把数据在回调回V层,P层的每个方法其实是独立的。但在ViewModel的实现中,应该更进一步的将数据封装在ViewModel中,而数据的增查改删都由ViewModel开放接口给V层调用,数据改变之后直接setValue到LiveData中通知到各个观察者。
一个具体的例子,还是影片信息的页面,之前的Presenter或者原来我们写ViewModel的时候会把FilmInfo的load,修改其中某个状态(点赞、想看)抽成一个个相互独立的接口,然后View层调用接口,根据接口的成功失败来做页面的刷新。但其实真正的MVVM里,V层应该更薄,所有的数据处理都在ViewModel中完成,ViewModel在拿到M层的接口数据后,不是简单的处理下就回调给View,而是直接修改FilmInfo数据,而View层的各个组件只管监听FilmInfo的变化就可以做到实时更新。

可能的坑

把LiveData当做event使用也是有一些可能的坑的,下面是通过自己试验和网上资料整理的风险点,但需要说明的是这些都是特定情境下不满足某种需求,需要根据业务的实际需要来决定是否值得考虑:

  1. setValue()在放入User的时候必须在主线程,否则会报错,而postValue则没有这个检查,而是会把数据传入到主线程。
  2. 使用LiveData监听数据变化,LifeCycleOwner在inactive状态下,不会观察到value变化(除非使用observeForever替代observe,但是使用observeForever需要自己管理observer,手动remove),在页面回到active状态后,会收到刚刚变化的值的最后一个value,中间的变化过程会丢失;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容