MVVM和DataBinding

简介

MVVM:MVP的升级版,ViewModel(vm)替换Presenter(p), ViewModel配合xml实现view和model的绑定

DataBinding:Google提出的数据绑定框架,可以轻松实现mvvm

MVVM的目的

实现应用之间数据与视图的分离、视图与业务逻辑的分离、数据与业务逻辑的分离,从而达到低耦合、可重用性、易测试性等好处。相对于mvp而言解耦更彻底,更易于进行单元测试

使用

配置

app的build文件加上:

 android {
    ...
    dataBinding{
        enabled =true;
    }
    ...
}

数据绑定

ViewModel:

ViewModel需继承BaseObservable,实现的ViewModel为自己定义的统一的接口
ViewModel中可以更新view的状态以及显示内容,可以绑定点击事件,显示图片等一系列与数据相关的ui操作
注意点
1.显示图片需要使用自定义属性@BindingAdapter,方法必须以static修饰,@BindingAdapter({"imageUrl"})imageUrl作为自定义属性在xml中使用
2.notifyPropertyChanged可以刷新具体某一属性,此方法必须配合@Bindable使用,加上这个注解后,DataBinding框架会在BR这个生成类中,为特定属性生成一个唯一的标识符。@Bindable最好注解在getter方法上而非注解在属性上
3.ObservableInt此类的ObservableField数据类型不需要注解即可绑定view,同样的String对应的为ObservableField<String>,但为确保性能此种数据类型尽量少用

/**
 * View model for each item in the repositories RecyclerView
 */
public class ItemRepoViewModel extends BaseObservable implements ViewModel {

    private Repository repository;
    private Context context;
   
    public String firstName;
    
    public ObservableInt tvKindVisibility;  //ObservableInt 不需要注解(get方法)即可绑定view的数据类型
    public String imageUrl="";

    public ItemRepoViewModel(Context context, Repository repository) {
        this.repository = repository;
        this.context = context;
    }

    public String getName() {
        return repository.name;
    }

    public String getDescription() {
        return repository.description;
    }

    public String getStars() {
        return context.getString(R.string.text_stars, repository.stars);
    }

    public String getWatchers() {
        return context.getString(R.string.text_watchers, repository.watchers);
    }

    public String getForks() {
        return context.getString(R.string.text_forks, repository.forks);
    }
    
    @Bindable
    public String getFirstName() {
        return context.getString(R.string.text_forks, repository.forks);
    }

   /**
     * 点击事件
     * @param view
     */
    public void onItemClick(View view) {
        context.startActivity(RepositoryActivity.newIntent(context, repository));
    }
    
    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }

    /**
     * 使用ImageLoader显示图片  方法必须为static修饰
     * @param imageView
     * @param url
     */
    @BindingAdapter({"imageUrl"})
    public static void imageLoader(ImageView imageView, String url) {
        Glide.with(imageView.getContext()).load(url)
                .signature(GloableData.getSignatureString())
                .into(imageView);
    }

    // Allows recycling ItemRepoViewModels within the recyclerview adapter
    public void setRepository(Repository repository) {
        this.repository = repository;
        notifyChange();   //主动刷新所有数据 更新ui
        notifyPropertyChanged(BR.firstName);  //主动刷新单个数据 更新ui 此属性需要@Bindable
    }

    @Override
    public void destroy() {
        //In this case destroy doesn't need to do anything because there is not async calls
    }

}

xml文件:

需要使用<layout></layout>作为根节点,在<layout>节点中我们可以通过<data>节点来引入我们要使用的数据源,可以使用诸如@{viewModel.onItemClick}的方式使用<data>引入的ViewModel,可以直接使用ViewModel中定义的属性和方法,并且属性的变化会自动反馈给view完成ui的更新
注意点:
1.<layout></layout>节点下是没有“layout_width”和“layout_height”的
2..<data>下引用的数据包名必须写全

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="uk.ivanc.archimvvm.viewmodel.ItemRepoViewModel" />
    </data>

    <android.support.v7.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/vertical_margin_half"
        android:layout_marginLeft="@dimen/vertical_margin"
        android:layout_marginRight="@dimen/vertical_margin"
        android:layout_marginTop="@dimen/vertical_margin_half"
        card_view:cardCornerRadius="2dp">

        <LinearLayout
            android:id="@+id/layout_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="?attr/selectableItemBackground"
            android:onClick="@{viewModel.onItemClick}"
            android:orientation="vertical">

            <TextView
                android:id="@+id/text_repo_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:maxLines="1"
                android:paddingLeft="12dp"
                android:paddingRight="12dp"
                android:paddingTop="12dp"
                android:text="@{viewModel.name}"
                android:textSize="20sp"
                tools:text="Repository Name" />

            <TextView
                android:id="@+id/text_repo_description"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingBottom="12dp"
                android:paddingLeft="12dp"
                android:paddingRight="12dp"
                android:paddingTop="10dp"
                android:text="@{viewModel.description}"
                android:textColor="@color/secondary_text"
                android:textSize="14sp"
                tools:text="This is where the repository description will go" />

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="@color/divider" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="60dp"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/text_watchers"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:text="@{viewModel.watchers}"
                    android:textColor="@color/secondary_text"
                    tools:text="10 \nWatchers" />

                <TextView
                    android:id="@+id/text_stars"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:text="@{viewModel.stars}"
                    android:textColor="@color/secondary_text"
                    tools:text="230 \nStars" />

                <TextView
                    android:id="@+id/text_forks"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:text="@{viewModel.forks}"
                    android:textColor="@color/secondary_text"
                    tools:text="0 \nForks" />

                <ImageView
                    android:layout_width="48dp"
                    android:layout_height="48dp"
                    android:layout_centerVertical="true"
                    android:layout_marginLeft="4dp"
                    android:layout_toRightOf="@id/layout_left"
                    android:visibility="@{viewModel.imgvTrendVisibility}"
                    app:imageUrl="@{viewModel.imageUrl}" />
            </LinearLayout>

        </LinearLayout>

    </android.support.v7.widget.CardView>

</layout>

DataBinding与ViewModel的绑定:

通过DataBindingUtilsetContentView进行binding初始化操作,setViewModel(此方法名与xml中data的定义相关)完成与ViewModel的绑定。binding可以替代butterknife直接获取控件并且使用,如下binding.ptrList,其中控件名ptrList由xml定义的id自动生成。

activity:
 //MainViewModel 类名由xml文件 R.layout.main_activity自动生成
 private MainActivityBinding binding;
 private MainViewModel mainViewModel;  
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //activity中binding的初始化方式
        binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
        mainViewModel = new MainViewModel(this, this);
        binding.setViewModel(mainViewModel);
        setSupportActionBar(binding.toolbar);
        setupRecyclerView(binding.reposRecyclerView);
    }
fragment和adapter:

binding.getRoot()获取根布局,即原本的ContentView

if (binding == null) {
            //fragment和adapter中binding的初始化方式
            binding = DataBindingUtil.inflate(inflater, R.layout.fragment_quote_databinding, container, false);
            binding.setViewModel(viewModel);
            init();
        }else {
            if (binding.getRoot().getParent() != null) {
                ((ViewGroup) binding.getRoot().getParent()).removeView(binding.getRoot());
            }
        }
        return binding.getRoot();
        
//通过binding可直接获取xml中控件 不需要findViewById
binding.ptrList.getRefreshableView().setSelector(R.color.trans);

绑定listview:

xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="adapter"
            type="android.widget.BaseAdapter" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:adapter="@{adapter}" />
    </LinearLayout>
</layout>

通过binding直接设置adapter

binding.setAdapter(mAdapter);

更多使用:

空指针

自动生成的DataBinding代码会检查null,避免出现NullPointerException
例如在表达式中@{user.phone}如果user == null那么会为user.phone设置默认值null而不会导致程序崩溃(基本类型将赋予默认值如int为0,引用类型都会赋值null)

自定义DataBinding名

<data class="MainBinding">
  ....
</data>

class对应的就是生成的Data Binding名

导包

布局文件中支持import的使用,原来的代码是这样

<data>
   <variable name="user" type="com.example.gavin.databindingtest.User" />
</data>

import后

 <data>
      <import type="com.example.gavin.databindingtest.User"/>
      <variable
          name="user"
          type="User" />
  </data>

遇到相同的类名的时候:

<data>
  <import type="com.example.gavin.databindingtest.User" alias="User"/>
  <import type="com.example.gavin.mc.User" alias="mcUser"/>
  <variable name="user" type="User"/>
  <variable name="mcUser" type="mcUser"/>
</data>

使用alias设置别名,这样user对应的就是com.example.gavin.databindingtest.User,mcUser就对应com.example.gavin.mc.User,然后

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>

当需要用到一些包时,在Java中可以自动导包,不过在布局文件中就没有这么方便了。需要使用import导入这些包,才能使用。如,需要用到View的时候

<data>
  <import type="android.view.View"/>
</data>
...
<TextView
...
android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
/>

注意:只要是在Java中需要导入包的类,这边都需要导入,如:Map、ArrayList等,不过java.lang包里的类是可以不用导包的

显示图片

除了文字的设置,网络图片的显示也是我们常用的。来看看Data Binding是怎么实现图片的加载的。
首先要提到BindingAdapter注解,这里创建了一个类,里面有显示图片的方法。

public class ImageUtil {
  /**
   * 使用ImageLoader显示图片 必须是public static的
   * @param imageView
   * @param url
   */
  @BindingAdapter({"bind:image"})
  public static void imageLoader(ImageView imageView, String url) {
      ImageLoader.getInstance().displayImage(url, imageView);
  }
}

这里只用了bind声明了一个image自定义属性,等下在布局中会用到。
这个类中只有一个静态方法imageLoader,里面有两参数,一个是需要设置图片的view,另一个是对应的Url,这里使用了ImageLoader库加载图片。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">

  <data >
      <variable
          name="imageUrl"
          type="String"/>
  </data>

  <LinearLayout

      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      android:gravity="center"
      >
      <ImageView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          app:image = "@{imageUrl}"/>
  </LinearLayout>
</layout>

最后在MainActivity中绑定下数据就可以了

binding.setImageUrl(
  "http://115.159.198.162:3000/posts/57355a92d9ca741017a28375/1467250338739.jpg");

表达式

三元运算

在User中添加boolean类型的isStudent属性,用来判断是否为学生。

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.isStudent? "Student": "Other"}'
android:textSize="30sp"/>

注意:需要用到双引号的时候,外层的双引号改成单引号

??

除了常用的操作法,另外还提供了一个 null 的合并运算符号 ??,这是一个三目运算符的简便写法。

contact.lastName ?? contact.name

相当于

contact.lastName != null ? contact.lastName : contact.name

ObseravbleField

google为我们提供了一些Obserable类:ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, ObservableParcelable

public static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

ObseravbleCollection

此种类型数据和ObseravbleField一样不需要注解,即不要@Bindable的get和set方法
注意:此类数据在使用的过程中注意初始化,否则会经常出现空指针异常

ObservableArrayMap

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在xml中使用:

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap<String, Object>"/>
</data>

…
<TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

ObservableArrayList

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);

xml使用:

<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
   android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

单元测试

1.MVVM的实现过程中应尽量将需要测试的逻辑转移到ViewModel中,进行单元测试时主要测试ViewModel
2.实现过程中某些逻辑与view结合紧密,此时需要灵活使用接口,通过回调的形式来实现。对于复杂页面而言可能会导致接口中的方法过多,需斟酌

遇到的问题

xml中定义出错,编辑器不会给出提示,导致binding找不到又很难定位出错的位置,使用时需谨慎

总结

MVVM的引入对于口袋贵金属项目而言是为了更好的进行单元测试,此外结合DataBinding的MVVM还有取代ButterKnife,ViewHolder等优势
对于单元测试,这里需要遵循三个规范(详细可参考我的自选模块的实现):
1.需要测试的逻辑尽量在ViewModel中实现,尽量脱离view
2.需要测试的逻辑需要抽离出相应的方法,并且方法应遵循单一原则
3.输入输出需要public暴露以方便断言(具体参考项目中已有的测试用例)

参考

demo

https://github.com/ivacf/archi

博客

http://www.jianshu.com/p/ba4982be30f8
https://news.realm.io/cn/news/data-binding-android-boyar-mount/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容