Android模块开发框架 LiveData+ViewModel

前言

为何选择LiveData+ViewModel

  • LiveData+ViewModel是Android Architecture Component开发组件的一部分,主要的目的是为了解决android开发过程中的因为Activity及Fragment生命周期引起的一些常见问题,譬如:内存泄露,异步任务引起空指针,横竖屏切换界面刷新问题,当然它的作用远不止于此,比如:LiveData的观察者模型可以保障界面在第一时间更新到最新的数据(当然你的LifecycleOwner必须是Alive状态),解决了多端写入数据的同步问题;使用LiveData实现View和VM的自动绑定(通常这个绑定的数据流向是单向的,VM->View).另外值得一提的是,AAC框架内部维护了一个ViewModel的内存缓存池,并且会监听Activity或Fragment的生命周期,在destory的时候自动清空缓存.因此,对于开发者而言,只需要聚焦在业务开发,几乎不用对接生命周期接口.
  • 感兴趣的同学可以去看看官方的详细文档和Demo
    • 官当文档:链接
    • 官方Demo:链接

MVP还是MVVM?

  • mvvm相比mvp最大的区别就是实现了v和vm(p)的自动绑定,mvp中的v和p之间存在较多的接口依赖,不利于扩展及测试,mvvm通常存在一个Binding中介层,通过注解+apt(或反射)的方式,解除v和vm直接的接口依赖,当然mvvm相比于mvp的进步不仅仅是代码解耦,也是从"面向功能接口编程"到"响应式编程"的思想转变,一切皆是数据(指令)流(ui<->数据<->model)
  • 官方推荐使用MVVM框架,结合DataBinding依赖注入框架实现View和VM的双向绑定,考虑到使用DataBinding依赖于xml布局配置,且有较大的理解成本,我们这次没有采用严格意义上的MVVM框架,而是选择折中方案:
    • VM->View:通过LiveData实现数据的单向流动
    • View->VM:依然采用传统的接口实现,但是所有的执行结果都依赖LiveData回传给View

经典的依赖原则

  • 一个框架的好坏,通常会有以下几个衡量指标:
    • 是否可以解决当前的业务问题
    • 是否具备好的可扩展性
    • 是否具备好的可测试性
    • 是否遵循模块化设计原则
    • 逻辑,界面,数据是否分离
  • 当然,还有更多的衡量指标,我们在这里不一一列举,上图所示的是一个圆环依赖结构,从内到外分别是:业务数据->业务逻辑层->接口适配层->界面,遵循"依赖倒置原则",内部圆圈不能依赖外部圆圈

框架介绍

模块的内部层级

  • 遵从单向依赖原则,我们的模块内部也划分了一下三个层级,从下往上分别是:
    • 数据层:
      • 主要用来提供界面展示及交互所需要数据,通常会定义获取数据的策略接口,选择不同的实现(DB,内存,网络等)
      • 不依赖其他层级,被逻辑层依赖
    • 逻辑层(领域层)
      • 这一层跟业务强相关,包含复杂的业务逻辑,譬如:获取数据,提交数据,数据存储策略的选择及数据融合等
      • 依赖数据层,被展示层依赖
    • 展示层(表现层)
      • 这一层的主要工作有以下几个:
        • 构建用户可见的界面
        • 为界面展示提供必要的数据
        • 接收并处理用户交互事件
      • 复杂的业务逻辑都委派给逻辑层(领域层)来处理,这里的ViewModel可以理解成一个接口适配器,只负责建立与View之间的通信渠道,然后传递数据或接受指令,自身并不处理复杂的业务逻辑
      • 依赖逻辑层(领域层)

各层级介绍

展示层:使用LiveData实现MVVM的单向绑定

  • View与VM之间的通信有两种
    • View->VM,通常是用户交互行为产生的一些指令(可能携带一些数据,譬如:用户登录行为会携带账号密码)
    • VM->View,通常是界面展示所需要的数据(也可能是状态,譬如:加载数据失败,展示一个Toast提示等)
  • 我们来举一个简单的案例,一个列表界面,需要刷新数据并展示,会有以下几个必要步骤:
    • 首先,View持有一个ViewModel实例(自己实例化,或则外部传参都可以)
    • 通过ViewModel获取一个LiveData对象(同一类LiveData在ViewModel内只能有一个实例),并开始观察这个LiveData对象(俗称subscribe)
    • ViewModel接收到"刷新数据"的指令,委派给具体的UseCase来执行
    • UseCase从数据源获取到数据,写入到LiveData
    • LiveData通知所有观察者(当然,会先判断observer依附的LifecycleOwner是否alive),其中就包括View
    • View从LIveData中获取到最新的完整的数据列表,刷新展示界面

逻辑层:UseCase处理复杂逻辑

  • 前面已经提到了,usecase主要用来处理复杂的业务逻辑,减轻ViewModel负担
  • BaseUseCase可以看做是一个模板方法类(当然这个模板不一定适用所有业务场景),内部会做一些"线程调度""LiveData赋值"等业务无相关的操作,具体的业务逻辑交给子类实现
  • 这里有一个Either返回值,这个是java 8函数式编程的一个特性,类似于c语言里的union(共同体),主要用来以类型安全的方式返回两个(或多个)值,感兴趣的同学可以自行google

数据层:Repository策略

  • 定义一个获取(读/写)数据的策略接口,实现不同的数据读写策略,也可以是多个策略的组合使用,根据具体的业务场景来决定,最大的好处就是可扩展性好,逻辑层(领域层)不用关心数据具体从哪里来

使用指南

如何界定一个独立的子模块

  • 模块划分有两种典型的思路,"按功能用途分模块","按业务特性分模块",前者的一个常规做法就是按照Model,View,Present(Controler)等角色对文件进行分组,这样做最大的弊端就是不利于业务拆分及多人协作编程,所以,我们推荐按照"业务特性分模块",譬如:主界面,详情页,登录页等都是一个相对独立的模块
  • 然后,如何界定一个独立的子模块,需要满足下面几个条件:
    • 相对独立的界面展示(android里的一个Activity或一个Fragment)
    • 相对独立的数据来源(你的界面渲染所需要的数据,可以通过独立的数据仓库获取,譬如:独立的服务端api接口,独立的数据表)
    • 用户交互产生的影响尽可能的收敛在界面内(譬如:下拉刷新产生的数据只用来渲染当前页面)
    • 具备一个闭环的生命周期(模块使用的内存是可回收的,不建议用单例来实现跨模块内存共享)
  • 简单概括就是:如果一个模块在脱离其他模块的情况下,依然能以缺省的方式独立运行,那么它就是一个相对独立的模块

搭建一个子模块

  • 我们以一个列表界面为例子,运行效果:

  • 按照以下步骤开发
    • Step1 数据层:数据仓库实现
      • 定义数据Bean
      public class HotContentItem {    public String id;    public String name;    public String desc;    public long timeStamp;}复制代码
      • 数据仓库策略实现(数据是本地mock的)
      public class HotContentNetRepository {    //mock数据    public Either<? extends Failure, List> refreshNew() {        try {            Thread.sleep(1000L);        } catch (InterruptedException e) {            e.printStackTrace();        }        Either<? extends Failure, List> result;        Random random = new Random();        boolean success = random.nextInt(10) > 3;        if (success) {            result = Either.right((mockItemList(0)));        } else if (random.nextInt(10) > 3) {            result = Either.right(Collections.emptyList());        } else {            result = Either.left(new NetworkFailure());        }        return result;    }}复制代码
    • Step2 逻辑层:数据仓库选择及使用
      • 省略列这一步,按照业务需求实现不同的数据仓库组合使用
    • Step3 逻辑层:实现UseCase(示例代码:刷新数据)
    public class HotContentRefreshNew extends BaseUseCase<List<HotContentItem>, Void> {    private HotContentNetRepository mNetRepository;    public HotContentRefreshNew(        MutableLiveData> data,        MutableLiveData failure) {        super(data, failure);        mNetRepository = new HotContentNetRepository();    }    @Override    protected Either<? extends Failure, List> loadData(Void aVoid) {        //从网络获取数据        Either<? extends Failure, List> result = mNetRepository.refreshNew();        if (result.isRight() && CollectionUtil.isEmpty(result.right())) {            Failure failure = new RefreshNewFailure(RefreshNewFailure.CODE_DATA_EMPTY, "Data is empty!");            result = Either.left(failure);        }        return result;    }    @Override    protected Failure processFailure(Failure failure) {        ...    }}复制代码
    • Step4 展示层:UI框架选择
      • 示例界面是作为一个TabLayout的一个Page页,因此这里选择"具备生命周期View"作为的UI框架,这是个自定的View,实现了LifecycleOwner接口(参考了LifecycleActivity和LifecycleFragment的实现逻辑)
      public abstract class BaseLifecycleView extends FrameLayout implements LifecycleOwner {    private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);    private ViewModelStore mViewModelStore = new ViewModelStore();    public BaseLifecycleView(@NonNull Context context) {        super(context);    }    protected abstract void onCreate();    protected abstract void onDestroy();    @Override    public Lifecycle getLifecycle() {        return mRegistry;    }    @Override    @CallSuper    protected void onAttachedToWindow() {        super.onAttachedToWindow();        mRegistry.handleLifecycleEvent(Event.ON_CREATE);        onCreate();        if (getVisibility() == View.VISIBLE) {            mRegistry.handleLifecycleEvent(Event.ON_START);        }    }    @Override    @CallSuper    protected void onDetachedFromWindow() {        super.onDetachedFromWindow();        mRegistry.handleLifecycleEvent(Event.ON_DESTROY);        mViewModelStore.clear();        onDestroy();    }    @Override    @CallSuper    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {        super.onVisibilityChanged(changedView, visibility);        Event event = visibility == View.VISIBLE ? Event.ON_RESUME : Event.ON_PAUSE;        mRegistry.handleLifecycleEvent(event);    }    @Override    @CallSuper    public void onStartTemporaryDetach() {        super.onStartTemporaryDetach();        State state = mRegistry.getCurrentState();        if (state == State.RESUMED) {            mRegistry.handleLifecycleEvent(Event.ON_STOP);        }    }    @Override    @CallSuper    public void onFinishTemporaryDetach() {        super.onFinishTemporaryDetach();        State state = mRegistry.getCurrentState();        if (state == State.CREATED) {            mRegistry.handleLifecycleEvent(Event.ON_START);        }    }    protected  T getViewModel(@NonNull ViewModelProvider.NewInstanceFactory modelFactory,                                                   @NonNull Class modelClass) {        return new ViewModelProvider(mViewModelStore, modelFactory).get(modelClass);    }}复制代码
    • Step5 展示层:定义自己的LiveData和ViewModel
    public class HotContentViewModel extends BaseViewModel<List<HotContentItem>> {    private HotContentRefreshNew mRefreshNew;    public HotContentViewModel() {        refreshNew();    }    public void refreshNew() {        AssertUtil.mustInUiThread();        if (mRefreshNew == null) {            mRefreshNew = new HotContentRefreshNew(getMutableLiveData(), getMutableFailure());        }        //通过usecase执行具体的刷新操作        mRefreshNew.executeOnAsyncThread(null);    }   ...}复制代码
    • Step6 展示层:关联V和VM
    public class HotContentView extends BaseLifecycleView {    private HotContentViewModel mViewModel;    private SwipeRefreshLayout mSwipeRefreshLayout;    private AutoLoadMoreRecycleView mRecyclerView;    private HotContentAdapter mContentAdapter;    public HotContentView(@NonNull Context context) {        super(context);        视图对象初始化        ...        mRecyclerView.setLoadMoreListener(new LoadMoreListener() {            @Override            public void onLoadMore() {                HotContentItem lastOne = CollectionUtil.lastOne(mViewModel.getData().getValue());                if (lastOne == null) {                    mRecyclerView.completeLoadMore("No more data");                } else {                    mViewModel.loadHistory(lastOne);                }            }        });        ...        mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {            @Override            public void onRefresh() {                //刷新数据                mViewModel.refreshNew();            }        });    }    @Override    protected void onCreate() {        mViewModel = getViewModel(new NewInstanceFactory(), HotContentViewModel.class);        mViewModel.getData().observe(this, new Observer>() {            @Override            public void onChanged(@Nullable List hotContentItems) {                //刷新数据成功                mContentAdapter.setItemList(hotContentItems);                mSwipeRefreshLayout.setRefreshing(false);                ...            }        });        ...    }    @Override    protected void onDestroy() {    }}复制代码

遇到的问题

  • 复杂的UI交互指令如何传达给ViewModel
    • 在本文开头"MVP还是MVVM"框架选型中我们已经提过,目前并没有使用到MVVM的精髓"DataBinding",而是通过LiveData观察者模式实现V->VM的单向绑定(即:数据可以从VM自动流向V,但是V的操作指令无法自动传递给VM),因此,复杂交互(譬如:下拉刷新,滚动加载更多)还是需要通过传统的MVP思维在VM中定义功能接口提供给V来调用
  • 除了数据之外,还有状态会影响界面展示
    • 理想状态下,VM提供一个LiveData给View使用,这个LiveData包含了View渲染需要的全部数据,但是很多情况下View并不会只依赖单一类型数据,譬如:下拉刷新操作,会有以下三种结果返回:列表数据,空数据,失败.对于"列表数据"我们可以通过LiveData通知View做整体刷新,但是"空数据""失败"的情况也需要在界面上有所提示,而这两个返回值是不能影响当前的"列表数据"(即:不影响当前的列表展示),而应该看做是独立与数据之外的"指令"更合适,它们最大的特征就是"一次性",不需要像"列表数据"那样存储处理(可以理解成是给界面消费的一次性事件)
    • 再回到LiveData,LiveData主要用来存储相对持久的数据,并且任何时候View从LiveData获取的数据都必须是"完整的"可以用来直接渲染界面的,回到上面"下拉刷新"的例子,如果我们将"空数据""失败"也通过LiveData封装,然后由View来观察这个LiveData(自定义一个Observer),在收到对应的"指令"通知的时候处理"界面提示",这样似乎也能满足VM->View的状态通知需求,问题来了,由于Observer的生命周期很可能会比LiveData的生命周期更短(取决于Observer依赖的LifecycleOwner)(比如:Observer的生命周期和ViewPager里的某一个View一致,LiveData的生命周期和Activity一致),那么当View被复用的时候会再次观察同一个LiveData,然后自动收到LiveData的通知,获取LiveData最新的数据(譬如:"失败"指令),刷新界面(提示"刷新失败"),这样就会很奇怪了,明明没有刷新动作,平白无故提示"刷新失败"
    • 解决办法还是回到"指令"的特征"一次性",定义一个DisposableLiveData,每次执行setData(会通知观察者,也就是View)之后立即将data置空,这样下次再getData时候就会返回null,而不是一个"未预期的数据"
      • 代码实现很简单
      public class DisposableLiveData<T> extends MutableLiveData<T> {    @Override    public void postValue(T value) {        super.postValue(value);        if (value != null) {            super.postValue(null);        }    }    @Override    public void setValue(T value) {        super.setValue(value);        if (value != null) {            super.postValue(null);        }    }    复制代码
      • 示例代码:
      public class HotContentView extends BaseLifecycleView {    private HotContentViewModel mViewModel;    private SwipeRefreshLayout mSwipeRefreshLayout;    public HotContentView(@NonNull Context context) {        super(context);        ...        mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {            @Override            public void onRefresh() {               //刷新数据                mViewModel.refreshNew();            }        });    }    @Override    protected void onCreate() {        mViewModel = getViewModel(new NewInstanceFactory(), HotContentViewModel.class);       ...        mViewModel.getFailure().observe(this, new Observer() {            @Override            public void onChanged(@Nullable Failure failure) {                //处理失败提示                if (failure instanceof RefreshNewFailure) {                    mSwipeRefreshLayout.setRefreshing(false);                    ToastManager.getInstance().showToast(getContext(), ((RefreshNewFailure)failure).getMessage(),                        Toast.LENGTH_SHORT);                }                ...            }        });    }    @Override    protected void onDestroy() {    }}复制代码

转载于:https://juejin.im/post/5cd6751bf265da03ab23455a

更多相关文章

  1. “罗永浩抖音首秀”销售数据的可视化大屏是怎么做出来的呢?
  2. Nginx系列教程(三)| 一文带你读懂Nginx的负载均衡
  3. 不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...
  4. Android(安卓)下载Zip文件,并解压到本地,进行本地调用
  5. android简单应用(一)
  6. Android学习笔记2012年(上)
  7. Android网络请求
  8. android 简单实现,微信第三方登录
  9. Android(安卓)使用librtmp推流【音频采集模块】

随机推荐

  1. Retrofit简要介绍
  2. Android:Layout_weight属性解析
  3. Android问题笔记
  4. Android系统中设置TextView的行间距(非行
  5. Android中自定义switch控件样式
  6. Android(安卓)检测网络连接状态
  7. android:launchMode="singleTask" 与 onN
  8. ListView 列表视图
  9. 【Android】AIDL介绍和实例讲解
  10. android音频介绍