Data Binding Library的使用和MVVM的探索

准备工作

  • 支持版本
    SDK 最小版本 Android 2.1(API level 7+)
    Android Studio 1.3及以上
    Gradle 1.5.0-alpha1及以上(检查 project/build.gradle)

  • 环境搭建

    1. 从SDK Manager 中下载支持库(android support repository)
    2. app/build.gradle下配置
android {
     ....    
     dataBinding {        
        enabled = true    
     }
}

入门使用

  • xml文件中
    1. 最外层布局为layout元素(即)
    2. 内容为两部分,第一部分 有元素 包裹,主要作用是引入java类;第二部分为 布局内容
    
        

name表示引用名  type表明具体是引用了哪个实体
  • 示例
    1 activity_main.xml


             
            
            
                 
             
        

写完此布局后会自动生成一个java类,此类根据布局名称,如:

        activity_main.xml   -->   ActivityMainBinding.java
        main_activity.xml   -->  MainActivityBinding.java
        note: 生成的java类不会出现在 src里面,因此可以在其他java文件中  调用一下,进行校验。如果没有生成,rebuild一下即可

2 MainActivity.java

          public class MainActivity extends AppCompatActivity {

            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
                binding.tvName.setText("zzb");
                binding.tvPassword.setText("123456");
            }
          }

运行可以得到对应的结果


Data Binding Library的使用和MVVM的探索_第1张图片
运行效果图

除了以上这种方试之外,还有一种在xml文件中引入变量,在java文件中导入实体数据,见代码
1 activity_main.xml



    
        
    

    

        

        
    

2 MainActivity.java

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
//        binding.tvName.setText("zzb");
//        binding.tvPassword.setText("123456");
     Student student = new Student("zzb", "123456");
     binding.setVariable(com.zzb.databindingtest.BR.stu, student);
    }
}
setVariable(int id, Object object) 
第一个参数是变量id (BR.id)
第二个参数是变量所对应的数据

效果同上

data部分详细介绍及布局内使用

  • import 的使用
    import标签和java中的import作用一样,都是导入类,这里导入的是java类,不管是sdk内的还是自己新建的

    
    
    

    如果存在java类名称相同,但是路径不同,此时需要使用 alias属性,指定一个别名,在后面的布局文件中直接使用别名代替,以下为举例说明

 
 

在布局内可以直接使用导入类的静态方法,注意方法必须是public static

android:text="@{String.valueOf(1234)}"

android:text="@{JString.valueOf(1234)}"
  • variable 的使用
    变量,意味着导入一个java变量,相当于是导入类的实例化对象
 

name表示在此xml文件中的变量名,type指定所对应的类型,在布局中的使用和平时使用java实例化对象一样,注意需要有访问权限(不管是变量还是方法,必须是public,变量private时必须有get/set方法)


  • include 标签 databinding的使用(此部分摘自官方文档),略做翻译
    variables需要通过使用应用命名空间(xmlns:app="http://schemas.android.com/apk/res-auto" 这种)和变量名(标签variable指定的name)从包含的布局中传递到一个被包含的布局,见下面的例子


   
       
   
   
       
       
   

然后,name.xml和contact.xml文件中必须包含一个user的变量()
DataBinding不支持merge标签,因此下面的layout是不对的



   
       
   
   
       
       
   

  • data 标签指定 class
    给data标签指定一个class,生成的bind class会换成你指定的,而不是之前默认生成的,示例如下
文件名: frag_image.xml




这样生成的bind class 为 ImageFrag,而不是未指定时默认生成的FragImageBinding,因此在对应的java文件中使用要改成ImageFrag

  • expression language(EL表达式的使用)
    这个xml文件中和java文件中的使用是一样的
数学运算符 + - / * %    (1+2)
String连接符 +          (android:text='@{"123" + "123"}')
逻辑运算符 && ||       
二进制 & | ^
一元运算符 + - ! ~
移位 >> >>> <<
比较 == > < >= <=
instanceof
Grouping ()
character, String, numeric, null
类型转换
方法调用
类属性
数组 []
三元运算符 ?:

dataBinding也提供一个判空运算符??,使用如下

 android:text="@{user.displayName ?? user.lastName}"

作用是,如果左边的user.displayName不为空则取左边的,否则取右边的,等同于

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

集合的使用,示例如下


    
    
    
    
    
    
    
    

…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

这里type里面的 <>使用(& lt;) (& gt;)代替,&后面的空格不要

绑定使用

  • 数据绑定
    直接是使用导入进来的variable,然后调用其属性即可



    

        
    

    

        

        

    

@={}表示当EditText内容发生更改时,所对应的属性值也会发生对应更改

  • 资源绑定
    资源的绑定无非就是使用drawable,dimen,string,color等资源,暂不支持mipmap
    android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
    // strings.xml中 %s is a title
    android:text="@{@string/name_format(viewModel.name)}" 
    这里name_format对应于strings.xml中的name,括号内的值对应于 %s,如果没有%s等 占位符,那么空格内就不需要有任何参数
    

string-array使用 @stringArray/xxx,对应的java数据是 string[] array
android:src="@{@drawable/ic_add}"

  • 事件绑定
    xml中可支持的事件好像只有onClick(如果还有别的请告知~)
    绑定onClick事件有两种,一种像之前那样,写好一个方法,参数为View view
  public class ImageItemViewModel extends ItemViewModel {

    public void showImageMessage(View view) {
        ToastUtil.showShortToast(view.getContext(), view.getTag().toString());
    }
}

在布局中直接使用




  

      
  

  
      
  

-
android:onClick="@{viewModel.showImageMessage}"
也可以写成
android:onClick="@{viewModel::showImageMessage}"

另外一种方式类似于lambda表达式,java中可以定义任意的方法,对参数也没有要求,使用时 ()->x() 这种方式即可

public class ImageItemViewModel extends ItemViewModel {

  public void showImageMessage() {
      ...
  }
}

布局内的使用




    

        
    

    
        
    

  • 数据的绑定
    单向绑定:当实体数据发生变化时,会通知view进行UI的刷新
    双向绑定:在单向绑定的基础上,当UI上的内容发生更改,实体数据也会发生变化,最简单的例子就是EditText
    如何定义单向绑定呢,官方提供两种方式,一种是继承BaseObservable(这是google给我们提供的Observable实现类,也可以自己实现);另一种是使用ObservableField,这种就不需要继承BaseObsevable
    先看看第一种的使用
private static class User extends BaseObservable {
    private String firstName;
    private String lastName;
    @Bindable
    public String getFirstName() {
        return this.firstName;
    }
    @Bindable
    public String getLastName() {
        return this.lastName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
        notifyPropertyChanged(BR.lastName);
    }
 }

BR是自动生成的一个文件,主要内容是一些Bind的属性值

另一种使用示例

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

这种的使用就不需要继承什么东西,并且ObservableField可以满足任何的字段,也可以使用ObservableInt,ObservableList,ObservableMap等。当需要设置value时,直接使用set方法,获取则使用get方法

firstName.set(str);
String str= firstName.get();

注意:ObservableField等变量其描述符不能使用static,否则当数据发生变更时页面不会发生相应的变化

java 类操作

  • Activity
    绑定使用,注意生成的Binding类和布局文件关,如果你设置了data的class属性则为其他,见上面的data标签指定class部分
ActivityLoginBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_login);
binding.setViewModel(new LoginViewModel(this, this));
  • Fragment
private FragImageBinding binding;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    binding = FragImageBinding.inflate(inflater, container, false);
    binding.setView(this);
    binding.setViewModel(new ImageViewModel(getContext(), this));
    return binding.getRoot();
}
  • AdapterView的Adapter
    这里针对RecyclerView和ListView/GridView做两种区分
    ListView/GridView绑定示例
@Override
public View getView(int position, View convertView, ViewGroup parent) {
   ViewDataBinding binding;
   if (convertView == null) {
       binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutID, parent, false);
   } else {
       binding = DataBindingUtil.getBinding(convertView);
   }
   T item = datas.get(position);
   binding.setVariable(BR.item, item);
   return binding.getRoot();
}

RecyclerView的绑定

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutID, parent, false);
  return new MyViewHolder(binding);
}
@Override
public void onBindViewHolder(CommonRecyclerAdapter.MyViewHolder holder, int position) {
  T item = datas.get(position);
  holder.bind(item);
}
class MyViewHolder extends RecyclerView.ViewHolder {
  private ViewDataBinding binding;
  public MyViewHolder(ViewDataBinding binding) {
      super(binding.getRoot());
      this.binding = binding;
  }
  public void bind(ItemViewModel viewModel) {

      binding.setVariable(BR.viewModel, viewModel);
      binding.executePendingBindings();
  }
}

关于此部分在后面将会对MVVM架构有一个自己的思考,里面包含此部分两个通用Adapter,有什么需要改善的地方请不吝赐教

  • 对View进行操作
    使用databinding后,我们基本上不会在Activity/Fragment等直接使用控件,如果有需求的话,那么有两种方式得到控件对象
    1. 传统的findViewById
    2. binding.viewId

高级使用

到了这里说明大概知识都已经了解,感觉databinding其实好像也没啥东西,无非就是写写数据,Activity/fragment等绑定一下,再处理一下事件,但是这里还有很多没有处理的,比如xml中未支持的事件呢,还有,databinding查找到xml属性所对应的方法是setXxx,如果实际上不是这种情况呢,以下

  • BindingMethod
    顾名思义,就是绑定方法,这里也是将属性值绑定到对应的方法中,解决上面最后所说的问题,比如,ImageView 有一个布局属性为 src,那么根据databinding默认的对应规则,它会被转换为 setSrc? 但是 ImageView 类并没有这么一个方法,而是有一个不叫这个名字的方法用来设置它的图片源setImageResource,这时我们需要使用这种绑定
@BindingMethods({
        @android.databinding.BindingMethod(type = android.widget.ImageView.class,
                attribute = "android:src",
                method = "setImageResource")

注意:这部分是写在class Xxx上面

  • BindingAdapter
    大致翻译是绑定适配,稍微理解下,大概就是绑定的时候适配下,使能够完成绑定,因此肯定是需要一个适配规则。而且,它的作用是什么呢,这里先列举这个需求,比如说我们的ListView需要onItemClick事件,但是默认没有提供,这时要怎么办呢,还有,RecyclerView,不想在java文件中设置它的LayoutManager,就想在xml文件中完成;还有,当我们的ImageView要加载一个网络图片,同样我不想在Activity文件中得到它的引用再来加载,这样会让我感觉有点如鲠在喉,这些,我们都可以使用BindingAdapter来自定义一个属性用来加载图片
@BindingAdapter(value = {"image_src", "placeholder"}, requireAll = false)
  public static void setImage(ImageView imageView, String path, Drawable drawable) {
        RequestCreator creator = Picasso.with(imageView.getContext()).load(path);
        if (drawable != null) {
            creator.placeholder(drawable);
        } else {
            creator.placeholder(R.mipmap.ic_image_placeholder);
        }
        creator.into(imageView);
 }

在使用时直接使用这个属性,前面加上一个自动命名空间,像app等


  

这里对@BindingAdapter(value = {"image_src", "placeholder"}, requireAll = false) 这句解释下, 首先使用BindingAdapter,这个没问题,value是一个数组,代表着多个参数,后面的requireAll表示所有的参数是否必须都有,true表示所有的参数必须都有,f否则可以存在参数没有的情况,上面ImageVIew使用了一个属性,如果要使用两个呢,如下


注意参数的类型要匹配,drawable目前只支持drawable文件夹下的资源,mipmap不能使用;这里是两个参数的使用,如果是三个,四个是一样的道理,只需要在xml文件中挨个设置即可,不是函数内一股脑都设置的。

对MVVM模式的思考

声明:此处纯属瞎扯,欢迎板砖加建议
MVC,MVP,MVVC的对比分析请自行百度,此处仅对MVVM瞎扯一番。
首先,MVVM分为三部分,M代表Model,和MVC,MVP中的一样,但是这里我仅表示实体类(ps:好像不是这样~),V代表View,主要是Activity,Fragment和布局文件,VM代表ViewModel,和View进行交互并且Databinding也是和这层绑定,同时和Model成进行交互,实际上,我的VM占据着和View打交道及具体逻辑操作的作用,还有一种方式就是VIewModel仅做跳板,真正的操作放在Model层(个人理解),这里盗图一张

Data Binding Library的使用和MVVM的探索_第2张图片
[来源](http://upload-images.jianshu.io/upload_images/2379203-4ed10a2a03c46154.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

然后这里要谨记一个事情,View层不做逻辑的处理,这个原则要坚守!

下面是我自己的目录结构,由于是在实践学习中的例子,因此结构不是很好,个人觉得真正做项目时可以按照功能分大模块,然后大模块内小模板按照MVVM三层结构来分,这样可能更清楚些

Data Binding Library的使用和MVVM的探索_第3张图片
目录结构

data文件夹我是作为model层来放,因此在这里主要是greendao自动生成的文件已经一些实体类,service文件夹存放retrofit需要的service,bindingadapter是一些自定义的bindingAdapter存放处,作为单独的一部分,便于移植,未来如果积累的多可以提出来作为library模块(实际上已经有人这样做了,后面的参考文件 一个github开源库,有需要BindingAdapter的使用就是这样的,还有参考这个开源库做出来功能更丰富的另一个国人开源的项目 MVVMLight)

下面示例下具体流程




    

        
        
    

    

        

        

        

效果如下


Data Binding Library的使用和MVVM的探索_第4张图片
xml界面效果

这里导入了两个variable,其中一个是View,这个在上面的Fragment绑定已经说过了,还有一个是ViewModel,这个是比较关键,看下它的内容

public class AddBookViewModel extends ViewModel {

    public final ObservableField name = new ObservableField<>();
    public final ObservableField content = new ObservableField<>();

    public AddBookViewModel(Context context, Presenter presenter) {
        super(context, presenter);
    }

    public void save() {
        BookDao bookDao = Session.getInstance(context).getBookDao();
        Book book = new Book(null, name.get(), content.get());
        book.setContent(content.get());
        long l = bookDao.insert(book);
        if (l >= 0) {
            replaceFragment(TaskFragment.newInstance());
        } else {
          showToast("保存失败");
        }
    }

    public void cancel() {
        replaceFragment(TaskFragment.newInstance());
    }


}

ViewModel是一个基类,因为基本上每一个业务就会有对应的一个ViewModel,因此提出一个基类比较方便,这里先介绍下当前这个ViewModel的作用,再去看它的基类
注意到有两个变量,分别对应于xml里面两个EditText的值,save是保存按钮触发的操作,cancel是取消触发的操作,大体就是这样,然后看下ViewModel基类

public class ViewModel {

    protected Context context;
    private Presenter presenter;

    public ViewModel(Context context, Presenter presenter) {
        this.presenter = presenter;
        this.context = context;
    }

    public Presenter getPresenter() {
        return presenter;
    }

    public void setPresenter(Presenter presenter) {
        this.presenter = presenter;
    }

    public void startActivity(String activityName) {
        presenter.startActivity(activityName);
    }

    public void startActivity(Class cls) {
        presenter.startActivity(cls);
    }

    public void backPress() {
        presenter.backPress();
    }

    public void showToast(String text) {
        presenter.showToast(text);
    }

    public void replaceFragment(BaseFragment fragment) {
        presenter.replaceFragment(fragment);
    }

    /\*\*
    \* OnRefreshListener刷新的实现方法,按需重写
     \*/
    public void onRefresh() {}

    /\*\*
     \* OnItemClickListener的实现方法,按需实现
     \* @param parent AdapterView
     \* @param view View
     \* @param position int 位置
     \* @param id long 返回的id
     \*/
    public void onItemClick(AdapterView parent, View view, int position, long id) {}
}

通过构造方法可以看到这里面有个Presenter接口,这里和MVP中的那个作用类似,主要是我很不想把一些页面跳转工作以及一些toast什么的放到ViewModel中,所以就用一个Presenter接口把这些操作都放到View中。然后还引用了一个Context对象,翻看文章有人说VM中不要引入Context,容易造成内存泄漏,但是这里我的一些操作必须要使用到Context对象,所以为了方便开发还是带入

经过上面两个操作,能够发现好像所有的事情都做完了,Activity,Fragment就需要绑定下就可以,做一些跳转处理等,所以内容会比较少

public class AddBookFragment extends BaseFragment {

    private FragAddBookBinding binding;

    public static AddBookFragment newInstance() {
        return new AddBookFragment();
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = FragAddBookBinding.inflate(inflater, container, false);
        binding.setView(this);
        binding.setViewModel(new AddBookViewModel(getContext(), this));
        return binding.getRoot();
    }
}

一个大概的流程就是这样,那么遇上ListView,RecycleView等怎么操作呢,毕竟还存在着一个Adapter,这里以一个网络请求图片,RecyclerView展示,使用SwipeRefreshLayout进行下拉刷新作为示例
Fragment所对应的布局内容




    

        

        

        

        
    

    

        
    

这里就不贴对应的Fragment代码了,因为和之前一样,仅仅只做一个绑定的作用,然后解释下SwipeRefreshLayout,RecyclerView下对应的几个属性

  • app:onRefresh="@{viewModel}"
    下拉刷新,这个是通过BindingAdapter定义的,做刷新的处理,这个前面不是看到了ViewModel中基类有一个onRefresh方法么,作用就在此处
@BindingAdapter({"onRefresh"})
public static void setOnRefreshListener(SwipeRefreshLayout swipeRefreshLayout, ViewModel viewModel) {
    swipeRefreshLayout.setOnRefreshListener(viewModel::onRefresh);
}
  • app:refreshing="@{viewModel.refreshing}"
    这个是做什么用呢,我们都知道SwipeRefreshLayout下拉下来会有一个加载圈,对,我们就是需要通过这个自定义的属性去控制它的展示与隐藏,而不是在Activity/Fragment里面处理
@BindingAdapter({"refreshing"})
public static void setRefreshing(SwipeRefreshLayout swipeRefreshLayout, boolean isRefreshing) {
    swipeRefreshLayout.setRefreshing(isRefreshing);
}
  • app:item_decoration="@{ItemDecorations.grid()}"
    这个作用很明显,就是设置ItemDecoration,是从一个github开源库,有需要BindingAdapter的使用偷学来的....

  • app:items="@{viewModel.picInfos}"
    app:item_layout="@{@layout/item_recycler_image}"
    app:layout_manager="@{LayoutManagers.grid(2)}"

    这里作用都比较明显,items指定绑定的数据,item_layout指定子View布局,layout_manager设置LayoutManager,下面给出RecyclerView的这一套设置的BindingAdapter

@SuppressWarnings("unchecked")
@BindingAdapter(value = {"items", "item_layout", "layout_manager", "item_decoration"},
        requireAll = false)
public static void setItems(RecyclerView recyclerView, List datas,
                            @LayoutRes int layoutID, LayoutManagers.LayoutManagerFactory managerFactory,
                            ItemDecorations.ItemDecorationFactory decorationFactory) {
    CommonRecyclerAdapter adapter = (CommonRecyclerAdapter) recyclerView.getAdapter();
    if (adapter != null) {
        adapter.setDatas(datas);
    } else {
        if (layoutID == -1) {
            throw new IllegalArgumentException("layoutID cannot -1");
        }
        adapter = new CommonRecyclerAdapter<>(datas, layoutID);
        recyclerView.setAdapter(adapter);
        if (managerFactory == null) {
            throw new IllegalArgumentException("layoutManager cannot null");
        }
        recyclerView.setLayoutManager(managerFactory.create(recyclerView));
        if (decorationFactory != null) {
            recyclerView.addItemDecoration(decorationFactory.create(recyclerView));
        }
    }
}
 
 

看到这里,生下的就是子View的一些处理,然后也没有发现其实Activity,Fragment基本上没有做什么事情了,只需要xml中写好那些对应的绑定,ViewModel中写好具体的操作,就做好了。

这里源码就不发了,全在一个平时写的demo项目下,比较杂乱,虽然也用了git,但是是私有的项目,很想开源出去啊,但是想想写成那样...革命尚未成功,同志仍需努力

参考资料

  • 一个最初始的例子
  • 对应官方文档的中文教程
  • 一些功能使用的具体示例blog
  • 一个github开源库,有需要BindingAdapter的使用
  • 官方文档中文版
  • 官方文档
  • 谷歌databinding示例项目

你可能感兴趣的:(Data Binding Library的使用和MVVM的探索)