Android DataBinding 问题记录

Android DataBinding 问题记录

06 May 2017

由于工作上有新项目,新项目,打算一切重零开始,所有都 Base 都重写,一切重新 原则。 如果需要其他项目的代码,也要 review 后,一行一行重写在新项目内。

架构方面,打算不再按照之前的 MVP 模式开发了,其实 todo-mvp-rxjava 还不错。

又回去看了看 googlesamples/android-architecture,发现存在 todo-mvvm-databinding 分之。所以,打算参照这个分支的架构去布局新项目。





正常添加依赖


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




ButterKnife 共存问题


其实,有了 databinding ,就不可能再需要 butterknife 了。

但是有些老项目存在 butterknife,此时引入 databinding 的话,那么会无法生成 binding 类。 有问题的配置:

apply plugin: 'me.tatarka.retrolambda'

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

dependencies {  
    // butterknife
    compile 'com.jakewharton:butterknife:8.4.0'
    apt 'com.jakewharton:butterknife-compiler:8.4.0'
}


查看 android-studio-2.3-migration 会发现:

If you use annotation processors (like ButterKnife for example):
remove the following line:
apply plugin: ‘android-apt’
change all occurrences of “apt” to “annotationProcessor”:
compile ‘com.jakewharton:butterknife:8.4.0’
annotationProcessor ‘com.jakewharton:butterknife-compiler:8.4.0’


所以,Android Studio 2.3 就没有 apt 了,会报错:Gradle DSL method not found: 'apt()'
此时,提供的 annotationProcessor 可以解决 butterknife 与 databinding 的共存问题

正确的配置:

apply plugin: 'me.tatarka.retrolambda'

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

dependencies {  
    // butterknife
    compile 'com.jakewharton:butterknife:8.4.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
    // databinding
    annotationProcessor 'com.android.databinding:compiler:2.3.0'
}

这种情况很少,我只是无意中碰到了,顺便 Fix 了。正常的话,移除 butterknife 也是最直接的方法。




Adapter 的问题


不管,是 RecyclerViewAdapter 还是 ListViewAdapter。在每个工作项目都会进行封装的,所以这里加上 DataBinding 就会更方便了。

这里涉及到 BR 的作用。BR:Binding resource
data 标签下声明的每一个 variable 标签 name, 都会在 BR 中自动生成对应的 variableId。比如:如果定义了一个 camnter 的 variable,就能在 BR 内看到 camnter 的 variableId。


BindingHolder

package com.camnter.databinding;

import android.databinding.ViewDataBinding;  
import android.support.annotation.NonNull;  
import android.support.v7.widget.RecyclerView;

/**
 * @author CaMnter
 */

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

    @NonNull
    private final T binding;


    public BindingHolder(@NonNull final T binding) {
        // binding.getRoot() = itemView
        super(binding.getRoot());
        this.binding = binding;
    }


    @NonNull
    public T getBinding() {
        return this.binding;
    }

}


写一个 XML仅仅为了在 BR 中定义几个 variableId。

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

    <data>

        <variable
            name="position"
            type="java.lang.Integer"/>

        <variable
            name="itemValue"
            type="java.lang.Object"/>

    </data>

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</layout>  

然后需要 type,这个 type 可以 随便写一个存在的类


BindingAdapter

package com.camnter.databinding;

import android.content.Context;  
import android.databinding.DataBindingUtil;  
import android.databinding.ViewDataBinding;  
import android.support.annotation.NonNull;  
import android.support.v7.widget.RecyclerView;  
import android.view.LayoutInflater;  
import android.view.ViewGroup;  
import java.util.ArrayList;  
import java.util.Collection;  
import java.util.List;

/**
 * @author CaMnter
 */

public abstract class BindingAdapter<T> extends RecyclerView.Adapter<BindingHolder> {

    private final Context context;
    private final List<T> list;
    private final LayoutInflater inflater;


    public BindingAdapter(Context context) {
        this.context = context;
        this.list = new ArrayList<>();
        this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }


    public Context getContext() {
        return this.context;
    }


    /**
     * Please return RecyclerView loading layout Id array
     * 请返回 RecyclerView 加载的布局 Id 数组
     *
     * @return 布局 Id 数组
     */
    public abstract int[] getItemLayouts();


    /**
     * Please write judgment logic when more layout
     * and not write when single layout
     * 如果是多布局的话,请写判断逻辑
     * 单布局可以不写
     *
     * @param position Item position
     * @return 布局 Id 数组中的 index
     */
    protected int getRecycleViewItemType(int position) {
        return 0;
    }


    /**
     * get the itemType by position
     * 根据 position 获取 itemType
     *
     * @param position Item position
     * @return 默认 ItemType 等于 0
     */
    @Override
    public int getItemViewType(int position) {
        return this.getRecycleViewItemType(position);
    }


    /******************
     * Magic extension
     ******************/

    @Override
    public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType < 0) return null;
        if (this.getItemLayouts() == null) return null;
        int[] layoutIds = this.getItemLayouts();
        if (layoutIds.length < 1) return null;

        int itemLayoutId;
        itemLayoutId = layoutIds.length == 1 ? layoutIds[0] : layoutIds[viewType];
        return new BindingHolder<>(
            DataBindingUtil.inflate(this.inflater, itemLayoutId, parent, false));
    }


    @Override
    public void onBindViewHolder(BindingHolder holder, int position) {
        T itemValue = null;
        final ViewDataBinding binding = holder.getBinding();
        if (position < this.list.size()) {
            itemValue = this.list.get(position);
        }
        binding.setVariable(com.camnter.databinding.BR.position, position);
        binding.setVariable(com.camnter.databinding.BR.itemValue, itemValue);
        binding.executePendingBindings();
    }


    @Override
    public int getItemCount() {
        return this.list.size();
    }


    /**********************
     * Some smart methods *
     **********************/

    public int getListSize() {
        return this.list.size();
    }


    public T getItem(int position) {
        return this.list.get(position);
    }


    public T getItemByPosition(int position) {
        return this.getItem(position);
    }


    public void setList(@NonNull final List<T> list) {
        this.list.clear();
        if (list.size() == 0) return;
        this.list.addAll(list);
        this.notifyDataSetChanged();
    }


    public void clear() {
        this.list.clear();
        this.notifyDataSetChanged();
    }


    public void remove(@NonNull final T t) {
        this.list.remove(t);
        this.notifyDataSetChanged();
    }


    @NonNull
    public List<T> getList() {
        return this.list;
    }


    public void addAll(@NonNull final Collection<T> list) {
        this.list.addAll(list);
        this.notifyDataSetChanged();
    }

}

利用了一个 XML,在 BR 中写了 variableId。所以,在 onBindViewHolder 方法内,自动 binding 了一些数据( positionitemValue )。


这样的话,业务 AdapterXML只需要有三个 variable 就可以position, itemValue )。




Inflate 问题


正常开发的话,我们在 Activity 内:

this.rootBinding = DataBindingUtil.setContentView(this, layoutId);  

那么在使用 Inflater 的地方需要注意了。

  • 自定义 View
  • Fragment
  • Inflate layout 到指定 layout

都会有 binding 的绑定问题。仔细探索的话,会发现 binding classcallbacks 总是为 null。导致,数据变了,View 不会变,因为源码中的 callbacks 总是为 null

this.rootBinding = DataBindingUtil.inflate(inflater, layoutId, container, false);  

但是,有一种写法,导致在 setVariable 的时候会:java.lang.IllegalArgumentException: View is not a binding layout
这种写法:

final View self = this.inflater.inflate(layoutId, container, false);  
this.rootBinding = DataBindingUtil.bind(self);  

推荐写法:

this.rootBinding = DataBindingUtil.inflate(inflater, layoutId, container, false);  
final View self = this.rootBinding.getRoot();  

自定义 View: 就把 false 改为 true 就可以了。




相关资料


DataBinding 模块

android-studio-2.3-migration




心得


推荐: 因为用 DataBinding 贯穿整个项目,所以尽量

1.DataBindingUtil.inflate 取代 Inflater.inflate
2.DataBindingUtil.setContentView 取代 Activity.setContentView

都会得到一个 ViewDataBinding,用 getRoot 就可以拿到 View 了。
这样可以减少一些关于 Binding 的错误。