流行Android知识Android开发

DataBinding学习与实践

2017-03-13  本文已影响677人  落魄的安卓开发

搞了搞DataBinding,并在项目新模块中进行了使用,感觉还是不错的,这里记录下使用过程及基本原理,方便以后查阅。

干啥的啊

DataBinding是啥?
一个Google官方的数据绑定框架-Data Binding Library,是对MVVM在Android上的一种实现,可以直接绑定数据到xml中,并实现自动刷新。

MVVM 又是啥?
MVVM是 Model-View-ViewModel的缩写。三部分分别是:Model – 代表你的基本业务逻辑;View – 显示内容;ViewModel – 将前面两者联系在一起的对象。MVVM 在使用当中,通常还会利用双向绑定技术即: Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。

1、准备工作

在Module的build.gradle android模块中添加

android {
 …
 dataBinding {
    enabled = true
 }
}

在布局Layout外面添加,<layout></layout>标签,示例如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

  <RelativeLayout
     android:id="@+id/activity_main"
     android:layout_width="match_parent"
     android:layout_height="match_parent">

     <TextView
         android:id="@+id/tvHelloWorld"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="Hello World!" />
  </RelativeLayout>
</layout>

好,准备到这里呢,我们就可以在Activity或者Fragment中使用DataBinding了。

2、在Activity、Fragment中如何初始化Binding类
 @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //setContentView(R.layout.activity_main);
    ActivityMainBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
}

 @Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    FragmentBinding inflate = DataBindingUtil.inflate(inflater, R.layout.fragment, container, false);
    return inflate.getRoot();
}

分别在Activity和Fragment中分别初始化了Binding类,可以看到,我们Binding类的生成是有规则的:activity_main-->ActivityMainBinding 、fragment-->FragmentBinding(不要忘记分别给这两个布局加<layout>标签),即:第一个单词首字母大写,第二个单词首字母大写...最后都会拼上Binding就是生成的Binding类。
如何自定义生成的Binding类名呢?如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
 <data class="CustomBindingName">
 </data>
 ...
</layout>

然后我们在生成Binding类的时候就可以:

CustomBindingName inflate = DataBindingUtil.inflate(inflater, R.layout.fragment, container, false);

初始化完Binding类之后,我们就可以直接使用该layout中定义了id的View,如:

viewDataBinding.tvHelloWolrd.setText("厉害了");

为什么可以直接拿来操作呢,因为DataBinding已经帮我们初始化好了(具体怎么初始化的下一篇再说,这篇就讲基本使用啦),这个已经初始化好的View的命名和Binding类的生成命名规则相同,只是后面没有加Binding了。

3、数据绑定
3.1、基本数据绑定

修改activity_main.xml文件如下(注意:这里不用判断bean!=null,因为DataBinding会自动帮助我们进行空指针的避免,比如@{bean.name},如果bean是null的话,bean.name则会被赋默认值(null)。age的话,则是0):

<layout xmlns:android="http://schemas.android.com/apk/res/android">
 <data>
    <variable
        name="bean"
        type="com.thc.bindingdemo.BindingBean" />
 </data>
 <LinearLayout
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tvHelloWolrd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <TextView
        android:id="@+id/tvName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{bean.name}" />

    <TextView
        android:id="@+id/tvAge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{String.valueOf(bean.age)}" />
 </LinearLayout>
</layout>

在java代码中:

BindingBean bindingBean = new BindingBean("thc",18);
//viewDataBinding.setVariable(com.thc.bindingdemo.BR.bean,demoBean);
viewDataBinding.setBean(demoBean);

(以上两种方式都可以)这样通过生成的Binding类给xml文件setBean就实现了View和Data的绑定。这样只能够实现给xml进行设置数据,但是View如何实现根据Data的变化实时更新呢?
就是让我们的Model extends BaseObservable,如果想更新单个属性就在该属性set方法中使用

//更新单个属性
notifyPropertyChanged(com.thc.bindingdemo.BR.school);
//更新所有的属性
notifyChange();
//定义的Bean如下:
public class BindingDemoBean extends BaseObservable {
 private String name;
 private String school;
 private String className;
 public BindingDemoBean(String name, String school,String className) {
     this.name = name;
     this.school = school;
      this.className = className;
 }
 public String getName() {
    return name;
 }
 public void setName(String name) {
     this.name = name;
     //更新所有的属性
     notifyChange();
 }
 @Bindable
 public String getSchool() {
     return school;
 }
 public void setSchool(String school) {
     this.school = school;
     //更新单个属性
     notifyPropertyChanged(com.thc.bindingdemo.BR.school);
 }
 public String getClassName() {
     return className;
 }
 public void setClassName(String className) {
    this.className = className;
 }
}
3.2数据绑定特殊的几个地方

1)集合(注意:需要判断一下集合的长度,否则有可能出现数组越界的错误)

 <variable
        name="beans"
        type="java.util.ArrayList<com.thc.bindingdemo.BindingDemoBean>" />

    <variable
        name="strings"
        type="java.util.ArrayList<String>" />

    <variable
        name="map"
        type="java.util.HashMap<String,String>" />

    <variable
        name="str"
        type="String" />

    <variable
        name="num"
        type="int" />

<!--需要注意的是,给ArrayList<...> 的<>进行了转义  < > -->
<variable
   name="beans"
   type="java.util.ArrayList<com.thc.bindingdemo.BindingDemoBean>" />
<!--取集合中的数据,设置姓名-->
<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{beans.size>0?beans.get(0).name:bean.name}" />

2)给TextView setText的时候既有动态又有写死的。注意:方式2使用单引号&&字符个数>2,如果只有一个比如"写"的话会报错(待搞)。

<!--写死的字符+动态字符 方式1-->
<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{beans.get(0).name + @string/app_name}" />

<!--写死的字符+动态字符 方式2-->
<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text='@{"写死的"+beans.get(0).name}' />
4、事件绑定
 <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="@{presenter.listenBind}"
        android:text="单独更新" />

<Button
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:onClick="@{()->presenter.lambda(bean)}"
   android:text="全部更新" />

//事件绑定
public class Presenter{
    /**
     * 实现单独更新某个属性
     * 方法绑定:这种方式要求,自定义的方法要和 public void onClick(View v) {} 一样,方法名可以不同,但是参数一定要有
     */
    public void listenBind(View view){
        demoBean.setSchool("北大");
        demoBean.setClassName("二班");
    }

    /**
     * 更新所有的属性
     * lambda表达式绑定:这种就可以任意定义了,这里是把绑定到xml的bean,传回来并show出来
     * 切记:如果使用lambda表达式绑定事件,在xml中调用它的方法的时候要加(),擦,吃了这里的亏
     */
    public void lambda(BindingDemoBean bean){
        demoBean.setName("很强势");
        demoBean.setClassName("三班");
        Toast.makeText(MainActivity.this,bean.toString(),Toast.LENGTH_SHORT).show();
    }
  
    public void lambda1(){
          Log.e("result","切记lambda表达式调用的时候要加()");
    }
}
 viewDataBinding.setPresenter(new Presenter());//这一步要记得哦
5、使用include及给include中的控件设置数据、绑定变量
<layout xmlns:android="http://schemas.android.com/apk/res/android">
 <data>
    <variable
        name="includeBean"
        type="com.thc.bindingdemo.BindingDemoBean" />

    <variable
        name="presenter"
        type="com.thc.bindingdemo.MainActivity.Presenter" />
 </data>

 <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:gravity="center_vertical"
    android:padding="10dp">

    <TextView
        android:id="@+id/tvBack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="@{presenter.finish}"
        android:text="返回" />

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="标题" />

    <TextView
        android:id="@+id/tvOperate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="分享" />

 </RelativeLayout>
</layout>

 viewDataBinding.includeBar.setPresenter(new Presenter());
 viewDataBinding.includeBar.setIncludeBean(demoBean);

使用Include还可以直接从外层布局传值到Include布局中去,如下所示:

   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.thc.myzhihu.bindingdemo.BindingIncludeActivity">

    <!--将传入外层布局的值,直接传入Include的布局中,-->
    <include
        layout="@layout/include_binding_test1"
        bind:includeBean="@{outterLayoutBean}"
        bind:presenter="@{outterPresenter}" />
</LinearLayout>
6、表达式 & 表达式链
6.1、表达式

这里只是说明下经常用的运算符:
注意:第一个三元运算符使用的时候要做如下操作

<data>
   ...
   <import type="android.view.View"/>
</data>

<!--三元运算符-->
<ImageView
   android:visibility="@{bean.age>10?View.VISIBLE:View.GONE}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:src="@mipmap/ic_launcher"/>
<!--空合并运算符 取两个中不为null的数据-->
<TextView
   android:text="@{beans.get(0).name??bean.name}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />
6.2、表达式链
<!--表达式链,就比如iv1、iv2的显示隐藏都和 bean的age大小有关,那么可以简化为如下方式-->
    <ImageView
        android:id="@+id/iv1"
        android:visibility="@{bean.age>10?View.VISIBLE:View.GONE}"
        android:src="@mipmap/ic_launcher"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ImageView
        android:id="@+id/iv2"
        android:visibility="@{iv1.visibility}"
        android:src="@mipmap/img2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
7、自定义BindAdapter
public class BindAdapter {
/**
 * 加载网络图片
 */
@BindingAdapter({"app:imageUrl","app:placeholderDraw"})
public static void setNetImg(ImageView ivNet, String imgUrl, Drawable placeHodler){
    Glide.with(ivNet.getContext()).load(imgUrl).placeholder(placeHodler).into(ivNet);
}
/**
 * 给将第一个字符变成红色
 */
@BindingAdapter("app:text")
public static void setSpannelText(TextView textView,String text){
    SpannableString spannableString = new SpannableString(text);
    ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.RED);
    spannableString.setSpan(colorSpan,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(spannableString);
}
}

在xml中的应用:

<!--自定义BindAdapter placeholderDraw和imageUrl必须和你的定义的静态方法中的参数一样的-->
<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:imageUrl="@{str}"
    app:placeholderDraw="@{@drawable/img2}" />
<!--自定义BindAdapter text必须和你的定义的静态方法中的参数一样的-->
<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  app:text='@{map.get("key")}' />
注意:在使用app:placeholderDraw="@{@drawable/img2}" 的时候,不能使用@mipmap/哦
8、自定Setter

针对自定义View

public class MyImageView extends ImageView {
 private String imgUrl;
 public MyImageView(Context context, AttributeSet attrs) {
    super(context, attrs);
 }
 public void setImgUrl(String imgUrl) {
    this.imgUrl = imgUrl;
    Glide.with(getContext()).load(imgUrl).into(this);
 }
}

<com.thc.bindingdemo.MyImageView
   app:imgUrl="@{myImageUrl}"
   android:layout_width="100dp"
   android:layout_height="100dp"/>
9、BindingConversion

用于时间转化,即我们传一个Date格式的数据给TextView,通过BindingConversion转化为你想要的时间格式,并设置给TextView,如下:

@BindingConversion
public static String convertTime(Date date) {
    SimpleDateFormat time = new SimpleDateFormat("yyyy-MM-dd");
    return time.format(date);
 }
<!--BindingConversion-->
<TextView
  android:text="@{time}"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>
 //设置数据
 viewDataBinding.setTime(new Date());
10、双向绑定
10.1 直接根据输入内容修改bean对象

如下:根据用户输入的学校,动态改变bean的学校值,同时上面tvSchool的显示值也变化了,挺好。

 <!--双向绑定,动态修改学校 这里注意 @={bean.school} @后面有个=号哦 -->
 <EditText
     android:layout_width="match_parent"
     android:layout_height="50dp"
    android:text="@={bean.school}"/>
10.2 监听输入内容(即addTextChanged效果)
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">

  <data>
      <variable
          name="inputText"
          type="String" />
      <variable
          name="presenter"
          type="com.thc.dialogfragmentdemo.MainActivity.MainPresenter" />
  </data>

  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      tools:context="com.thc.dialogfragmentdemo.MainActivity">
      <EditText
          android:id="@+id/edtTest"
          android:layout_width="match_parent"
          android:layout_height="50dp"
          android:afterTextChanged="@{()->presenter.afterTextChanged1(inputText)}"
          android:beforeTextChanged="@{()->presenter.beforeTextChanged(inputText)}"
          android:onTextChanged="@{()->presenter.onTextChanged(inputText)}"
          android:text="@={inputText}" />
          <!--同样-->
    </LinearLayout>
</layout>

Presenter代码如下:

  public class MainPresenter {
    public void onTextChanged(String s) {
        LogUtil.loge("MainPresenter+onTextChanged:" + s);
    }

    public void afterTextChanged1(String s) {
        LogUtil.loge("MainPresenter+afterTextChanged:" + s);
    }

    public void beforeTextChanged(String s) {
        LogUtil.loge("MainPresenter+beforeTextChanged:" + s);
    }
  }
11、RecyclerView中应用

示例如下:

customBindingName.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
customBindingName.recyclerView.setAdapter(new MyAdapter(getActivity(),initDatas()));
//适配器代码如下
class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyHolder>{
    Context mContext;
    List<BindingDemoBean> mDatas;
    public MyAdapter(Context conetxt,List<BindingDemoBean> list){
        this.mContext = conetxt;
        this.mDatas = list;
    }
    @Override
    public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflate = LayoutInflater.from(mContext);
        ViewDataBinding binding = DataBindingUtil.inflate(inflate, R.layout.item_recyclerview, parent, false);
        return new MyHolder(binding);
    }
    @Override
    public void onBindViewHolder(MyHolder holder, int position) {
        ViewDataBinding binding = holder.getBinding();
        BindingDemoBean bindingDemoBean = mDatas.get(position);
        //执行一下executePendingBindings,及时刷新
        binding.executePendingBindings();
        binding.setVariable(com.thc.bindingdemo.BR.item,bindingDemoBean);
    }
    @Override
    public int getItemCount() {
        return mDatas.size();
    }
    class MyHolder extends RecyclerView.ViewHolder{
        private ViewDataBinding mBinding;
        public MyHolder(ViewDataBinding mBinding) {
            super(mBinding.getRoot());
            this.mBinding = mBinding;
        }
        public ViewDataBinding getBinding(){
            return mBinding;
        }
    }
}

ViewHolder抽取出来:

public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {

 protected final T mBinding;

 public BindingViewHolder(T binding) {
    super(binding.getRoot());
    mBinding = binding;
 }

 public T getBinding() {
    return mBinding;
 }
}
13、自定义Component

问:我们都知道可以通过自定义BindingAdapter 提供View没有的setter方法或者,在执行View自身的setter之前进行一些操作。但是,系统是如何找到我们自定义的BindingAdapter并调用它内部的static静态方法的呢?

答:在build/intermediates/classes下面,可以找到DataBindingComponent类,包名为android.databinding,全局只会有一个该类——此接口在编译时生成,包含了所有用到的实例BindingAdapters的getter方法。
当一个BindingAdapter是一个实例方法(instance method),一个实现该方法的类的实例必须被实例化。这个生成的接口会包含每个声明BindingAdapter的类/接口的get方法。命名冲突会简单地加一个数字前缀到get方法前来解决。

使用步骤如下:

(1)比如我们这里需要实现一件换肤操作需要定义两个组件:DayComponent 和 NightComponent ,在自定义组件内部进行初始化对应的BindingAdapter,如下:

  /**
   * 白天组件
 */
 public class DayComponent implements android.databinding.DataBindingComponent {
    public MyBindingAdapter myBindingAdapter = new DayBindingAdapter();

    @Override
    public MyBindingAdapter getMyBindingAdapter() {
        return myBindingAdapter;
    }
}

(2)MyBindingAdapter是DayBindingAdapter,NightBindingAdapter的父类,来实现具体的换肤操作,DayBindingAdapter如下:

/**
 * 日间Adapter
 */
public class DayBindingAdapter extends MyBindingAdapter{

  @Override
  public void setBgColor(LinearLayout layout, int llBgColor) {
    layout.setBackgroundColor(layout.getResources().getColor(R.color.pop_bgcolor));
  }
  @Override
  public void setTvColor(TextView tv, int tvColor) {
      tv.setTextColor(tv.getResources().getColor(R.color.white));
  }

}
(3)首先在Application中设置默认的Component,setComponent(DayComponent),点击换肤的时候进行Day和Night的切换

Application中:
DataBindingUtil.setDefaultComponent(new DayComponent());

public void btn5(View v){
     if(MyApplication.isDay){
            DataBindingUtil.setDefaultComponent(new DayComponent());
     }else{
         DataBindingUtil.setDefaultComponent(new NightComponent());
     }
     MyApplication.isDay = !MyApplication.isDay;
     recreate();
}

Demo地址:https://github.com/tianHouChao/DataBindingDemo

接下来总结一下,DataBinding是如何初始化View,及与数据绑定实时刷新,及BindingAdapter原理等。
嗯,可以的!

感谢巨人:
大帅
亓斌

上一篇下一篇

猜你喜欢

热点阅读