如何构建Android MVVM 应用框架,高级Android开发必看

ViewModel层做的事情刚好和View层相反,ViewModel只做和业务逻辑和业务数据相关的事,不做任何和UI相关的事情,ViewModel 层不会持有任何控件的引用,更不会在ViewModel中通过UI控件的引用去做更新UI的事情。ViewModel就是专注于业务的逻辑处理,做的事情也都只是对数据的操作(这些数据绑定在相应的控件上会自动去更改UI)。同时DataBinding框架已经支持双向绑定,让我们可以通过双向绑定获取View层反馈给ViewModel层的数据,并对这些数据上进行操作。关于对UI控件事件的处理,我们也希望能把这些事件处理绑定到控件上,并把这些事件的处理统一化,为此我们通过BindingAdapter对一些常用的事件做了封装,把一个个事件封装成一个个Command,对于每个事件我们用一个ReplyCommand去处理就行了,ReplyCommand会把你可能需要的数据带给你,这使得我们在ViewModel层处理事件的时候只需要关心处理数据就行了,具体见**MVVM Light Toolkit 使用指南**的Command部分。再强调一遍:ViewModel 不做和UI相关的事。

Model

Model层最大的特点是被赋予了数据获取的职责,与我们平常Model层只定义实体对象的行为截然不同。实例中,数据的获取、存储、数据状态变化都是Model层的任务。Model包括实体模型(Bean)、Retrofit的Service ,获取网络数据接口,本地存储(增删改查)接口,数据变化监听等。Model提供数据获取接口供ViewModel调用,经数据转换和操作并最终映射绑定到View层某个UI元素的属性上。

如何协作


关于协作,我们先来看下面的一张图:

[图片上传失败…(image-cfc71b-1606550227256)]

图 1

上图反映了MVVM框架中各个模块的联系和数据流的走向,我们从每个模块一一拆分来看。那么我们重点就是下面的三个协作。

  • ViewModel与View的协作
  • ViewModel与Model的协作
  • ViewModel与ViewModel的协作

ViewModel与View的协作

[图片上传失败…(image-1be9c4-1606550227256)]

图 2

图2中ViewModel和View是通过绑定的方式连接在一起的,绑定分成两种:一种是数据绑定,一种是命令绑定。数据的绑定DataBinding已经提供好了,简单地定义一些ObservableField就能把数据和控件绑定在一起了(如TextView的text属性),但是DataBinding框架提供的不够全面,比如说如何让一个URL绑定到一个ImageView,让这个ImageView能自动去加载url指定的图片,如何把数据源和布局模板绑定到一个ListView,让ListView可以不需要去写Adapter和ViewHolder相关的东西?这些就需要我们做一些工作和简单的封装。MVVM Light Toolkit 已经帮我们做了一部分的工作,详情可以查看**MVVM Light Toolkit 使用指南**。关于事件绑定也是一样,MVVM Light Toolkit 做了简单的封装,对于每个事件我们用一个ReplyCommand去处理就行了,ReplyCommand会把可能需要的数据带给你,这样我们处理事件的时候也只关心处理数据就行了。

由 图 1 中ViewModel的模块中我们可以看出ViewModel类下面一般包含下面5个部分: >

  • Context (上下文) - Model (数据源 Java Bean) - Data Field (数据绑定) - Command (命令绑定) - Child ViewModel (子ViewModel)

我们先来看下示例代码,然后再一一讲解5个部分是干嘛用的:


//context

private Activity context;



//model(数据源 Java Bean)

private NewsService.News news;

private TopNewsService.News topNews;



//数据绑定,绑定到UI的字段(data field)

public final ObservableField<String> imageUrl = new ObservableField<>();

public final ObservableField<String> html = new ObservableField<>();

public final ObservableField<String> title = new ObservableField<>();

// 一个变量包含了所有关于View Style 相关的字段

public final ViewStyle viewStyle = new ViewStyle();



//命令绑定(command)

public final ReplyCommand onRefreshCommand = new ReplyCommand<>(() -> {    



})

public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((itemCount) -> { 



});



//Child ViewModel

public final ObservableList<NewItemViewModel> itemViewModel = new ObservableArrayList<>();



/** * ViewStyle 关于控件的一些属性和业务数据无关的Style 可以做一个包裹,这样代码比较美观,

ViewModel 页面也不会有太多太杂的字段。 **/

public static class ViewStyle {    

   public final ObservableBoolean isRefreshing = new ObservableBoolean(true);    

   public final ObservableBoolean progressRefreshing = new ObservableBoolean(true);

} 

Context

Context是干嘛用的呢,为什么每个ViewModel都最好需要持了一个Context的引用呢?ViewModel不处理和UI相关的事也不操作控件,更不更新UI,那为什么要有Context呢?原因主要有以下两点:

  1. 通过图1中,然后得到一个Observable,其实这就是网络请求部分。其实这就是网络请求部分,做网络请求我们必须把Retrofit Service返回的Observable绑定到Context的生命周期上,防止在请求回来时Activity已经销毁等异常,其实这个Context的目的就是把网络请求绑定到当前页面的生命周期中。

  2. 在图1中,我们可以看到两个ViewModel之间的联系是通过Messenger来做,这个Messenger是需要用到Context,这个我们后续会讲解。

当然,除此以外,调用工具类、帮助类有时候需要Context做为参数等也是原因之一。

Model (数据源)

Model是什么呢?其实就是数据源,可以简单理解是我们用JSON转过来的Bean。ViewModel要把数据映射到UI中可能需要大量对Model的数据拷贝和操作,拿Model的字段去生成对应的ObservableField然后绑定到UI(我们不会直接拿Model的数据去做绑定展示),这里是有必要在一个ViewModel保留原始的Model引用,这对于我们是非常有用的,因为可能用户的某些操作和输入需要我们去改变数据源,可能我们需要把一个Bean在列表页点击后传给详情页,可能我们需要把这个Model当做表单提交到服务器。这些都需要我们的ViewModel持有相应的Model(数据源)。

Data Field(数据绑定)

Data Field就是需要绑定到控件上的ObservableField字段,这是ViewModel的必需品,这个没有什么好说。但是这边有一个建议: 这些字段是可以稍微做一下分类和包裹的。比如说可能一些字段是绑定到控件的一些Style属性上(如长度、颜色、大小),对于这类针对View Style的的字段可以声明一个ViewStyle类包裹起来,这样整个代码逻辑会更清晰一些,不然ViewModel里面可能字段泛滥,不易管理和阅读性较差。而对于其他一些字段,比如说title、imageUrl、name这些属于数据源类型的字段,这些字段也叫数据字段,是和业务数据和逻辑息息相关的,这些字段可以放在一块。

Command(命令绑定)

Command(命令绑定)简言之就是对事件的处理(下拉刷新、加载更多、点击、滑动等事件处理)。我们之前处理事件是拿到UI控件的引用,然后设置Listener,这些Listener其实就是Command。但是考虑到在一个ViewModel写各种Listener并不美观,可能实现一个Listener就需要实现多个方法,但是我们可能只想要其中一个有用的方法实现就好了。更重要一点是实现一个Listener可能需要写一些UI逻辑才能最终获取我们想要的。简单举个例子,比如你想要监听ListView滑到最底部然后触发加载更多的事件,这时候就要在ViewModel里面写一个OnScrollListener,然后在里面的onScroll方法中做计算,计算什么时候ListView滑动底部了。其实ViewModel的工作并不想去处理这些事件,它专注做的应该是业务逻辑和数据处理,如果有一个东西不需要你自己去计算是否滑到底部,而是在滑动底部自动触发一个Command,同时把当前列表的总共的item数量返回给你,方便你通过 page=itemCount/LIMIT+1去计算出应该请求服务器哪一页的数据那该多好啊。MVVM Light Toolkit 帮你实现了这一点:


 public final ReplyCommand<Integer> onLoadMoreCommand =  new ReplyCommand<>((itemCount) -> { 

   int page=itemCount/LIMIT+1; 

   loadData(page.LIMIT)

}); 

接着在XML布局文件中通过bind:onLoadMoreCommand绑定上去就行了。


 <android.support.v7.widget.RecyclerView 

 android:layout_width="match_parent"  

 android:layout_height="match_parent"  

 bind:onLoadMoreCommand="@{viewModel.loadMoreCommand}"/>

 x```



 具体想了解更多请查看 **[MVVM Light Toolkit 使用指南]( )**,里面有比较详细地讲解Command的使用。当然Command并不是必须的,你完全可以依照自己的习惯和喜好在ViewModel写Listener,不过使用Command可以使ViewModel更简洁易读。你也可以自己定义更多的、其他功能的Command,那么ViewModel的事件处理都是托管ReplyCommand<T>来处理,这样的代码看起来会比较美观和清晰。Command只是对UI事件的一层隔离UI层的封装,在事件触发时把ViewModel层可能需要的数据传给ViewModel层,对事件的处理做了统一化,是否使用的话,还是看你个人喜好了。



 ### Child ViewModel(子ViewModel)



 子ViewModel的概念就是在ViewModel里面嵌套其他的ViewModel,这种场景还是很常见的。比如说你一个Activity里面有两个Fragment,ViewModel是以业务划分的,两个Fragment做的业务不一样,自然是由两个ViewModel来处理,这时候Activity对应的ViewModel里面可能包含了两个Fragment各自的ViewModel,这就是嵌套的子ViewModel。还有另外一种就是对于AdapterView,如ListView RecyclerView、ViewPager等。



 ```java

  //Child ViewModelpublic final 

   ObservableList<ItemViewModel> itemViewModel = new ObservableArrayList<>(); 

它们的每个Item其实就对应于一个ViewModel,然后在当前的ViewModel通过ObservableList持有引用(如上述代码),这也是很常见的嵌套的子ViewModel。我们其实还建议,如果一个页面业务非常复杂,不要把所有逻辑都写在一个ViewModel,可以把页面做业务划分,把不同的业务放到不同的ViewModel,然后整合到一个总的ViewModel,这样做起来可以使我们的代码业务清晰、简短意赅,也方便后人的维护。

总的来说,ViewModel和View之前仅仅只有绑定的关系,View层需要的属性和事件处理都是在XML里面绑定好了,ViewModel层不会去操作UI,只是根据业务要求处理数据,这些数据自动映射到View层控件的属性上。关于ViewModel类中包含哪些模块和字段,这个需要开发者自己去衡量,我们建议ViewModel不要引入太多的成员变量,成员变量最好只有上面的提到的5种(context、model……),能不引入其他类型的变量就尽量不要引进来,太多的成员变量对于整个代码结构破坏很大,后面维护的人要时刻关心成员变量什么时候被初始化、什么时候被清掉、什么时候被赋值或者改变,一个细节不小心可能就出现潜在的Bug。太多不清晰定义的成员变量又没有注释的代码是很难维护的。

另外,我们会把UI控件的属性和事件都通过XML(如bind:text=@{…})绑定。如果一个业务逻辑要弹一个Dialog,但是你又不想在ViewModel里面做弹窗的事(ViewModel不希望做UI相关的事)或者说改变ActionBar上面的图标的颜色,改变ActionBar按钮是否可点击,这些都不是写在XML里面(都是用Java代码初始化的),如何对这些控件的属性做绑定呢?我们先来看下代码:


public class MainViewModel implements ViewModel {

....

//true的时候弹出Dialog,false的时候关掉dialog

public final ObservableBoolean isShowDialog = new ObservableBoolean();

....

.....

}

// 在View层做一个对isShowDialog改变的监听

public class MainActivity extends RxBasePmsActivity {



private MainViewModel mainViewModel;



@Override

protected void onCreate(Bundle savedInstanceState) {

..... 

mainViewModel.isShowDialog.addOnPropertyChangedCallback(new android.databinding.Observable.OnPropertyChangedCallback() {

      @Override

      public void onPropertyChanged(android.databinding.Observable sender, int propertyId) {

          if (mainViewModel.isShowDialog.get()) {

               dialog.show();

          } else {

               dialog.dismiss();

          }

       }

    });

 }

 ...

} 

简单地说你可以对任意的ObservableField做监听,然后根据数据的变化做相应UI的改变,业务层ViewModel只要根据业务处理数据就行,以数据来驱动UI。

ViewModel与Model的协作


从图1中,ViewModel通过传参数到Model层获取网络数据(数据库同理),然后把Model的部分数据映射到ViewModel的一些字段(ObservableField),并在ViewModel保留这个Model的引用,我们来看下这一块的大致代码(代码涉及简单的RxJava,如看不懂可以查阅入门一下):


 //Model

 private NewsDetail newsDetail;



 private void loadData(long id) {  

   //  Observable<Bean> 用来获取网络数据

   Observable<Notification<NewsDetailService.NewsDetail>>   newsDetailOb =   

   RetrofitProvider.getInstance()

                  .create(NewsDetailService.class)   

                  .getNewsDetail(id)                   

                  .subscribeOn(Schedulers.io())      

                  .observeOn(AndroidSchedulers.mainThread())

                 // 将网络请求绑定到Activity 的生命周期

                  .compose(((ActivityLifecycleProvider) context).bindToLifecycle()) 

                 //变成 Notification<Bean> 使我们更方便处理数据和错误

                  .materialize().share();  



 // 处理返回的数据

   newsDetailOb.filter(Notification::isOnNext)         

               .map(n -> n.getValue())    

               // 给成员变量newsDetail 赋值,之前提到的5种变量类型中的一种(model类型)        

               .doOnNext(m -> newsDetail = m)   

               .subscribe(m -> initViewModelField(m));



 // 网络请求错误处理

    NewsListHelper.dealWithResponseError(

      newsDetailOb.filter(Notification::isOnError)        

                  .map(n -> n.getThrowable()));

}

//Model -->ViewModel

private void initViewModelField(NewsDetail newsDetail) {  

     viewStyle.isRefreshing.set(false);   

     imageUrl.set(newsDetail.getImage());    

     Observable.just(newsDetail.getBody())

            .map(s -> s + "<style type=\"text/css\">" + newsDetail.getCssStr())           

            .map(s -> s + "</style>")            

            .subscribe(s -> html.set(s));   

     title.set(newsDetail.getTitle());

 } 


版权声明:本文为m0_61417954原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。