うさがにっき

読書感想文とプログラムのこと書いてきます

DataBindingを使ったLayoutの作成(ソース編)

概要

tiro105.hateblo.jp
の続き
javaソースの視点からDataBindingの使い方を見てみる

詳細

Bindingクラスの作成方法

ActivityでのBindingクラスの作成にはDataBindingUtilクラスおsetContentView()メソッドを使ったが、このメソッドはActivityクラスのsetContentView()メソッドをラッピングしたものなので、Activity以外では使えない
DataBindingUtil | Android Developers
Activity以外ではBindingクラスが持つbind()とinflate()メソッドを用いる

bind()

Fragmentの場合、onViewCreatedなどviewを受け取る部分でbind()クラスを使い、Layoutをbindする

Public class MainActivityFragment extends Fragment {

    FragmentMainBinding fragmentMainBinding;

    public MainActivityFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        fragmentMainBinding = FragmentMainBinding.bind(view);

        Entity entity = new Entity();
        entity.setTitle("test");

        fragmentMainBinding.setEntity(entity);
    }
}
inflate()

inflate()はLayoutInflaterをわたすことで、レイアウトをinflateしつつ、Bindingクラスも作成するメソッド

public class EntityAdapter extends ArrayAdapter<Entity> {
    public EntityAdapter(Context context) {
        super(context, -1);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView == null) {
            EntityBinding entityBinding =
                    EntityBinding.inflate(LayoutInflater.from(getContext()));
            convertView = entityBinding.getRoot();
            convertView.setTag(entityBinding);
        }
        ((EntityBinding) convertView.getTag()).setEntity(getItem(position));
        
        return super.getView(position, convertView, parent);
    }
}

Observableによる値に自動反映

バインドする値が不変な場合、Bindingクラスに一度データをセットすればOKだが、可変の場合データを行使した時に再度Bindingクラスにデータをセットする必要がある

api.updateTitle(text, new Callback() {
  @Override
  public void onSuccess(String updated) {
    entity.setTitle(updated);

    // タイトルが更新されたのでもう一度bindingにわたす
    binding.setEntity(entity);
  }
});

これだと値が変化するたびにbindingに値を渡す必要があり、面倒くさい
android.databinding.Observableと@Bindableアノテーションを使うと更新の監視と反映を同時に行える

public class Entity implements android.databinding.Observable{
    public String title;

    PropertyChangeRegistry propertyChangeRegistry;

    public String getTitle() {
        return title;
    }

    @Bindable
    public void setTitle(String title) {
        this.title = title;
        propertyChangeRegistry.notifyChange(this, com.example.a01011818.fragmentdatabinding.BR.title);
    }

    @Override
    public void addOnPropertyChangedCallback(OnPropertyChangedCallback onPropertyChangedCallback) {
        propertyChangeRegistry.add(onPropertyChangedCallback);
    }

    @Override
    public void removeOnPropertyChangedCallback(OnPropertyChangedCallback onPropertyChangedCallback) {
        propertyChangeRegistry.remove(onPropertyChangedCallback);
    }
}

Observableを実装するとBindingクラス側でバインド処理する際にaddOnPropertyChangedCallback()をcallしてコールバックを登録してくれるようになる、データが更新された時に登録されたコールバックを呼び出せばBindingクラスのバインド処理が再度実行される。
また@BindableアノテーションをつけたことによりBR.javaが生成される
BR.javaには@Bindableアノテーションをつけたプロパティ名に対応するプロパティIDが定義される、Bindingクラスから受け取ったコールバックに対してプロパティIDを指定すると対応する値のバインド処理が実行される

これによりモデルに対して変更を行った時に自動的にViewに反映させることができる

api.updateTitle(text, new Callback() {
  @Override
  public void onSuccess(String updated) {
    entity.setTitle(updated);

    // 再度bindingに渡す必要がない
  }
});

BaseObservableを使うことによりもっと簡単に自動反映を行うことができる
>|java|
ublic class Entity extends BaseObservable{
    public String title;

    PropertyChangeRegistry propertyChangeRegistry;

    public String getTitle() {
        return title;
    }

    @Bindable
    public void setTitle(String title) {
        this.title = title;
        propertyChangeRegistry.notifyChange(this, com.example.a01011818.fragmentdatabinding.BR.title);
    }
}

ただしBaseObservableクラスは抽象クラスであるため、継承が行えないクラスでは自動更新を行えないことを留意しておく

Automatic Setterで存在しない属性値にbind

バインド処理は生成されたBindingクラスで行われる、つまりコード上で行われる。
この性質を使ってandroid名前空間に存在しない属性でも対象のViewのもつsetterメソッドに対応する値をバインドの対象にできる
この仕組みをAutomatic Setterという

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

    <data>
        <variable
            name="onItemClick"
            type="android.widget.AdapterView.OnItemClickListener"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            bind:onItemClickListener="@{onItemClick}"/>

    </LinearLayout>
</layout>
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        OnitemclickLayoutBinding binding = DataBindingUtil
                .setContentView(this, R.layout.onitemclick_layout);

        binding.setOnItemClick(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

            }
        });
    }
}
対応するsetterを持たない属性値を@BindAdapterを使って自作

Automatic Setterではandroid:paddingLeft属性のように対応するsetterメソッドを持たない属性をカバーできない
@BindingAdapterアノテーションを使うと特定のメソッドをバインド処理の際にも呼び出せるようになる、これによりsetterメソッドを持たない属性についてもバインドできるようになる

@BindingAdapter("capText")
public static void setCapText(TextView view, String text) {
    view.setText(text.toUpperCase());
}
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >
 
    <data>
        <variable
            name="user"
            type="com.example.a01011818.User" />
    </data>
 
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
 
        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            app:capText="@{user.name}"
            />
 
    </RelativeLayout>
</layout>

@BindingAdapterアノテーションを付与するには次の条件を満たす必要がある

  • staticメソッド
  • 第一引数に対象となるViewをとること
  • 第二引数以降に属性値を取ること

またメソッドを書く場所には決まりがなく、同じ属性について複数メソッドを定義することはできない
このことから専用のクラスなりを作り@BindingAdapterを付与したメソッドが一箇所に集まるようにすべき

感想

Automatic Setterや@BindingAdapterを使うことにより、様々な要素をBindngさせることができることがわかった
DataBindingがリリースされてから1年近く経っていることから、リリース当初出ていたバグもほぼなくなっていることがわかり十分実用レベルに達している

が、独自名前空間であるためオートコンプリートが効かなかったり、@BindingAdapterを使うと一気にソースを追うことが難しくなったりとButterKnifeでやっていた処理をすべてDataBindingで行うのは生産的でないと感じた

ButterKnifeで簡単にViewをオブジェクトをソースに落とし込んで、データの受け渡しはDataBinding、イベント処理などはソースで行うなどが現実的のように感じた。