Android模块开发框架 LiveData+ViewModel

栏目: IOS · Android · 发布时间: 5年前

  • LiveData+ViewModel是Android Architecture Component开发组件的一部分,主要的目的是为了解决android开发过程中的因为Activity及Fragment生命周期引起的一些常见问题,譬如:内存泄露,异步任务引起空指针,横竖屏切换界面刷新问题,当然它的作用远不止于此,比如:LiveData的观察者模型可以保障界面在第一时间更新到最新的数据(当然你的LifecycleOwner必须是Alive状态),解决了多端写入数据的同步问题;使用LiveData实现View和VM的自动绑定(通常这个绑定的数据流向是单向的,VM->View).另外值得一提的是,AAC框架内部维护了一个ViewModel的内存缓存池,并且会监听Activity或Fragment的生命周期,在destory的时候自动清空缓存.因此,对于开发者而言,只需要聚焦在业务开发,几乎不用对接生命周期接口.
  • 感兴趣的同学可以去看看官方的详细文档和Demo
    • 官当文档:链接
    • 官方Demo:链接
  • 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
Android模块开发框架 LiveData+ViewModel
  • 一个框架的好坏,通常会有以下几个衡量指标:
    • 是否可以解决当前的业务问题
    • 是否具备好的可扩展性
    • 是否具备好的可测试性
    • 是否遵循模块化设计原则
    • 逻辑,界面,数据是否分离
  • 当然,还有更多的衡量指标,我们在这里不一一列举,上图所示的是一个圆环依赖结构,从内到外分别是:业务数据->业务逻辑层->接口适配层->界面,遵循"依赖倒置原则",内部圆圈不能依赖外部圆圈
Android模块开发框架 LiveData+ViewModel
  • 遵从单向依赖原则,我们的模块内部也划分了一下三个层级,从下往上分别是:
    • 数据层:
      • 主要用来提供界面展示及交互所需要数据,通常会定义获取数据的策略接口,选择不同的实现(DB,内存,网络等)
      • 不依赖其他层级,被逻辑层依赖
    • 逻辑层(领域层)
      • 这一层跟业务强相关,包含复杂的业务逻辑,譬如:获取数据,提交数据,数据存储策略的选择及数据融合等
      • 依赖数据层,被展示层依赖
    • 展示层(表现层)
      • 这一层的主要工作有以下几个:
        • 构建用户可见的界面
        • 为界面展示提供必要的数据
        • 接收并处理用户交互事件
      • 复杂的业务逻辑都委派给逻辑层(领域层)来处理,这里的ViewModel可以理解成一个接口适配器,只负责建立与View之间的通信渠道,然后传递数据或接受指令,自身并不处理复杂的业务逻辑
      • 依赖逻辑层(领域层)
Android模块开发框架 LiveData+ViewModel
  • 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中获取到最新的完整的数据列表,刷新展示界面
Android模块开发框架 LiveData+ViewModel
  • 前面已经提到了,usecase主要用来处理复杂的业务逻辑,减轻ViewModel负担
  • BaseUseCase可以看做是一个模板方法类(当然这个模板不一定适用所有业务场景),内部会做一些"线程调度""LiveData赋值"等业务无相关的操作,具体的业务逻辑交给子类实现
  • 这里有一个Either<Failure, T>返回值,这个是 java 8函数式编程的一个特性,类似于 c语言 里的union(共同体),主要用来 以类型安全的方式返回两个(或多个)值,感兴趣的同学可以自行google
Android模块开发框架 LiveData+ViewModel
  • 定义一个获取(读/写)数据的策略接口,实现不同的数据读写策略,也可以是多个策略的组合使用,根据具体的业务场景来决定,最大的好处就是可扩展性好,逻辑层(领域层)不用关心数据具体从哪里来
  • 模块划分有两种典型的思路,"按功能用途分模块","按业务特性分模块",前者的一个常规做法就是按照Model,View,Present(Controler)等角色对文件进行分组,这样做最大的弊端就是不利于业务拆分及多人协作编程,所以,我们推荐按照"业务特性分模块",譬如:主界面,详情页,登录页等都是一个相对独立的模块
  • 然后,如何界定一个独立的子模块,需要满足下面几个条件:
    • 相对独立的界面展示(android里的一个Activity或一个Fragment)
    • 相对独立的数据来源(你的界面渲染所需要的数据,可以通过独立的数据仓库获取,譬如:独立的服务端api接口,独立的数据表)
    • 用户交互产生的影响尽可能的收敛在界面内(譬如:下拉刷新产生的数据只用来渲染当前页面)
    • 具备一个闭环的生命周期(模块使用的内存是可回收的,不建议用单例来实现跨模块内存共享)
  • 简单概括就是:如果一个模块在脱离其他模块的情况下,依然能以缺省的方式独立运行,那么它就是一个相对独立的模块
  • 我们以一个列表界面为例子,运行效果:
Android模块开发框架 LiveData+ViewModel
  • 按照以下步骤开发
    • 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<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());
            }
            //通过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<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() {
          }
      
      }
      复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

删除

删除

[英] 维克托•迈尔-舍恩伯格(Viktor Mayer-Schönberger)著 / 袁杰 译 / 浙江人民出版社 / 2013-1 / 49.90元

《删除》讲述了遗忘的美德,为读者展现了大数据时代的取舍之道。 《删除》从大数据时代信息取舍的目的和方法分别诠释了“被遗忘的权利”。维克托首先回溯了人类追寻记忆的过程,之后提出数字技术与全球网络正在瓦解我们天生的遗忘能力。对此,他考察了促进遗忘终止4大驱动力——数字化,廉价的存储器,易于提取,全球性访问。之后,他提出了当前数字化记忆的两大威胁——信息权力与时间,并给出了应对威胁的6大对策——数......一起来看看 《删除》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码