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
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<Failure, T>返回值,这个是java 8函数式编程的一个特性,类似于c语言里的union(共同体),主要用来以类型安全的方式返回两个(或多个)值,感兴趣的同学可以自行google
数据层:Repository策略
- 定义一个获取(读/写)数据的策略接口,实现不同的数据读写策略,也可以是多个策略的组合使用,根据具体的业务场景来决定,最大的好处就是可扩展性好,逻辑层(领域层)不用关心数据具体从哪里来
使用指南
如何界定一个独立的子模块
- 模块划分有两种典型的思路,"按功能用途分模块","按业务特性分模块",前者的一个常规做法就是按照Model,View,Present(Controler)等角色对文件进行分组,这样做最大的弊端就是不利于业务拆分及多人协作编程,所以,我们推荐按照"业务特性分模块",譬如:主界面,详情页,登录页等都是一个相对独立的模块
- 然后,如何界定一个独立的子模块,需要满足下面几个条件:
- 相对独立的界面展示(android里的一个Activity或一个Fragment)
- 相对独立的数据来源(你的界面渲染所需要的数据,可以通过独立的数据仓库获取,譬如:独立的服务端api接口,独立的数据表)
- 用户交互产生的影响尽可能的收敛在界面内(譬如:下拉刷新产生的数据只用来渲染当前页面)
- 具备一个闭环的生命周期(模块使用的内存是可回收的,不建议用单例来实现跨模块内存共享)
- 简单概括就是:如果一个模块在脱离其他模块的情况下,依然能以缺省的方式独立运行,那么它就是一个相对独立的模块
搭建一个子模块
- 按照以下步骤开发
- Step1 数据层:数据仓库实现
public class HotContentItem {
public String id;
public String name;
public String desc;
public long timeStamp;
}
复制代码
public class HotContentNetRepository {
public Either<? extends Failure, List<HotContentItem>> refreshNew() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Either<? extends Failure, List<HotContentItem>> 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.<HotContentItem>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<List<HotContentItem>> data,
MutableLiveData<Failure> failure) {
super(data, failure);
mNetRepository = new HotContentNetRepository();
}
@Override
protected Either<? extends Failure, List<HotContentItem>> loadData(Void aVoid) {
Either<? extends Failure, List<HotContentItem>> 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 extends ViewModel> T getViewModel(@NonNull ViewModelProvider.NewInstanceFactory modelFactory,
@NonNull Class<T> 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());
}
mRefreshNew.executeOnAsyncThread(null);
}
...
}
复制代码
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<List<HotContentItem>>() {
@Override
public void onChanged(@Nullable List<HotContentItem> 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<Failure>() {
@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