pixiv insideは移転しました! ≫ https://inside.pixiv.blog/

既存アプリにData Bindingを導入してみて

こんにちは。Android Studio 2.0の正式版が待ち遠しくてたまらない @rooandqoo です。 最近、今関わっているAndroidアプリ「pixivマンガ」にData Bindingを導入したので、所感をお話します。

Data Bindingとは

Google I/O 2015で発表された機能で、XMLに書いたViewのプロパティをバインドしてくれるスグレモノです。

従来は、たとえばfirstNamelastNameというプロパティを持つ User というモデルを定義し、画面に2つのプロパティを表示する場合、レイアウトファイルとActivityのコードは以下のようになっていました。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/firstName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
     />

    <TextView
        android:id="@+id/lastName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
     />
</LinearLayout>
class MainActivity extends Activity {

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        ((TextView) findViewById(R.id.firstName)).setText("Test");
        ((TextView) findViewById(R.id.lastName)).setText("User");
    }

Data Bindingを使うと、以下のように記述することができます。

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

    <data>
        <variable
            name="user"
            type="com.example.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.lastName}" />
    </LinearLayout>

</layout>
class MainActivity extends Activity {

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
        User user = new User("Test", "User");
        binding.setUser(user);
    }

Activityでモデルを定義し、自動生成されるクラスに渡してやれば、あとはレイアウト側がよしなに表示までをやってくれる、という塩梅になっています。

公式ドキュメントを見るに様々なことができそうですが、一番インパクトが大きいなと思ったのは Android のコードにありがちなfindViewById()setOnClickListener()を一掃することができたり、ListView などを使う上でほぼ必須といえる ViewHolder の実装が不要になる点です。

今回の変更では ButterKnife というライブラリを使ってViewのバインドを行っていた箇所をData Bindingを使うように置き換えたので、実際に変更を加えた部分について簡単にご紹介していきます。

導入

build.gradle に3行書き足すだけで使い始めることができます。

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

Viewをバインドしていた部分を置き換える

まず、レイアウトファイルのトップレベルを<layout></layout>で囲みます。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

...

</LinearLayout>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

...

    </LinearLayout>
</layout>

また、クラスのonCreate()内で、ButterKnifeを使ってバインドしていた部分は以下のように置き換えます。

public void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_xxxx);
    ButterKnife.bind(this);
}

public void onCreate(Bundle savedInstanceState) {
    ActivityXXXXBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_xxxx);
    binding.setActivity(this);
}

このbindingに、レイアウトのIDから自動生成されたメンバが含まれます。 例えばtitle_textというIDを降ったTextViewにテキストをセットしたい場合はbinding.titleText.setText("hoge")としてやると良いです。

ButterKnifeにおいて@OnClickなどでイベントを取得していた場合、XMLファイルの各アイテムに以下のような行を追加します。

<data>

    <variable
        name="activity"
        type="(Activityのパッケージ)" />
</data>
    
...
    
<Button>
    ...
    android:onClick="@{activity.onXXXClick}"
    ...
</Button>

AdapterクラスにData Bindingを実装する

ListViewGridViewを使う上で必要不可欠なAdapterクラスですが、ほとんどの場合 ViewHolder を実装してビューの使い回しを実現していると思います。 Data Bindingを使うと、ViewHolderを作らずともビューの使い回しが実現できて便利です。

既存のAdapterクラス内 getView() の実装はこのようになりました。

@Override
public View getView(final int position, View convertView, ViewGroup parent) {

    ViewHolder holder;
    if (convertView == null) {
        convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
        holder = new ContentViewHolder(convertView);
        convertView.setTag(holder);
    } else {
        holder = (ContentViewHolder) convertView.getTag();
    }
    
    holder.hogeText.setText("hoge");
}

...

public static class ViewHolder {
    
    @Bind(R.id.hoge)
    public TextView hoge;
    
    public ViewHolder(View view) {
        ButterKnife.bind(this, view);
    }
}

@Override
public View getView(final int position, View convertView, ViewGroup parent) {

    XXXBinding binding;
    if (convertView == null) {
        binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.list_item, parent, false);

        convertView = binding.getRoot();
        convertView.setTag(holder);
    } else {
        binding = (XXXBinding) convertView.getTag();
    }
    
    // ModelオブジェクトをXML内に持たせるだけで済む
    Model model = models.get(position);
    binding.setModel(model);
}

ViewHolderに持たせたViewに、いちいちオブジェクトをセットしなくて済むのも良いところです。

@BindingAdapterを使ってみる

ListViewのアイテムの中で、アイテムが持つパラメータの状態に合わせてViewを切り替えるといったことを実現したい時に便利なアノテーションが@BindingAdapterです。

以下のように、漫画を「読んだ」「読んでる途中」「まだ読んでいない」というステータスに合わせて、アイテムの背景色を変えるコードを実装してみます。 f:id:rooandqoo32:20160217005031p:plain:h400

まず、Adapter内に@BindingAdapterを付加したメソッドを実装します。(ViewHistoryは閲覧履歴を保存するためのモデルです)

@BindingAdapter("containerBackground")
public static void setContainerBackground(View container, ViewHistory viewHistory) {
    if (viewHistory.hasRead()) {
        itemContainer.setBackgroundResource(R.drawable.bg_item_viewed);
    } else {
        itemContainer.setBackgroundResource(R.drawable.bg_item);
    }
}

第一引数に対象のViewを、第二引数以降に、メソッド内で使用するパラメータを指定しているのがポイントです。 次に、XMLのアイテムに、アノテーションをつけたcontainerBackgroundを追記します。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    
    <data>
        <import type="jp.pxv.android.manga.model.ViewHistory" />

        <variable
            name="viewHistory"
            type="ViewHistory" />
    </data>    
    <LinearLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        ...
        app:containerBackground="@{viewHistory}">
    
    ...
    
    </LinearLayout>
</layout>

ViewHistoryを使うために<data></data>内にインポートが必要となり、少しだけXMLが複雑になりますが、AdapterのgetView()内で分岐を書くよりはいくらか綺麗になるのではないでしょうか。

導入のメリットとデメリット

Activityをはじめとしたクラス内にViewのメンバを用意する必要がなくなるため、Java側のコードがすっきりします。

特にAdapterを使うにあたってほぼ必須だったViewHolderクラスの生成が不要になったのは個人的に革命でした。

デメリットとしては、まず<layout></layout>で囲む必要があるために必然的にXMLファイルのネストが1段階深くなることが挙げられます。

アノテーションをつけて定義したメソッドがどこからでも参照できたり、XML内に変数を色々定義できるのも便利なのですが、気をつけないとすぐにどこに何があるのかわからなくなってしまうのも難点です。

まとめ

既存プロジェクトへのData Binding導入について簡単に解説しました。 公式の機能ということもありますので、新規開発の際には積極的に使っていくと良いと思います。 複数人で触るプロジェクトへの導入には、どこからどこまでをData Bindingに任せるかしっかり線引きをしておくと良さそうです。