美文网首页
MVVM和DataBinding

MVVM和DataBinding

作者: 34sir | 来源:发表于2017-08-10 17:26 被阅读68次

    简介

    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/

    相关文章

      网友评论

          本文标题:MVVM和DataBinding

          本文链接:https://www.haomeiwen.com/subject/qgohrxtx.html