package nagochi.blog;

主にプログラミングの備忘録を書いていこうと思います。最近はAndroidアプリがメインです。

【Android Data Binding】ListView に ObservableArrayList をバインドする

f:id:nagochi:20170322184819p:plain:w350

Android Data Binding で ListView にリストデータ(ObservableArrayList)をバインドできないかと調べてみたときの備忘録。

はじめに

例えば TextView に ObservableField<String> をバインドしたい場合、ソースコードは以下のようになります。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="viewModel" type="com.nagochi.ViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.text}" />
    </LinearLayout>
</layout>

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.setViewModel(new ViewModel());
    }
}

ViewModel.java

public class ViewModel {
    public final ObservableField<String> text = new ObservableField<>("テキスト");
}

f:id:nagochi:20170322180047p:plain:w350

これと同じように ListView にも ObservableArrayList<String> をバインドしたいのですが、標準ではそのような機能は付いていません。ListView をカスタムして自分で実装する必要があります。

というわけで、ListView を継承したクラス BindingListView を作っていこうと思います。最終的な目標は以下のような書き方でデータバインドが可能になることです。

<BindingListView
    android:id="@+id/bindingListView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:list="@{viewModel.list}" />
public class ViewModel {
    public final ObservableArrayList<String> list = new ObservableArrayList<>();
}

自動セッター setList を定義する

まずは xmlapp:list=“@{viewModel.list}”という式を記述できるようにします。これは簡単で、クラス内に自動セッターとして setList メソッドを作ればいいだけです。

public class BindingListView extends ListView {
    public BindingListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setList(ArrayList<String> list) {
    }
}

Android Data Binding の機能の一つです。クラス内に自動セッターpublic void setxxx(引数yyy)を定義しておくと、 xml にてapp:xxx="@{引数yyy}"という式を記述できるようになります。
データ バインディング ライブラリ | Android Developers #自動セッター

それでは xmlapp:list=“@{viewModel.list}”という式を記述しておくと何が起こるのか?ですが、viewModel.list にデータを追加するたびに setList が呼び出されるようになります。ですのでデータの追加時にするべき処理がある場合は、ここに記述すればいいということになります。

setList の中身を記述する

ということで、データの追加時にする処理を setList に記述していきます。

public class BindingListView extends ListView {
    private ArrayAdapter<String> adapter;

    public BindingListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setList(ArrayList<String> list) {
        if (adapter == null) {
            adapter = new ArrayAdapter<>(
                    getContext(), android.R.layout.simple_list_item_1, list);
            setAdapter(adapter);
        }

        adapter.notifyDataSetChanged();
    }
}

まずひとつ目は Adapter の作成です。送られてきた list を画面に表示させるためには、通常の ListView を扱うときもそうですが Adapter を作成する必要があります。Adapter は一度だけ作ればいいので、setList の初回呼び出し時のみ Adapter を作成します。

ふたつ目は ListView の表示更新です。すでに list にデータは追加されていますが、それだけでは画面に反映されません。adapter.notifyDataSetChanged();を実行し、 ListView の表示更新を促す必要があります。

以上で実装は完了です。

ソースコード全体

最低限のコードの他に、動作確認のために viewmodel.list に5秒毎にデータを追加するコードも記述しています。画像からでは分かりにくいですが、viewmodel.list にデータを追加した瞬間に ListView も更新され、項目が増えていることが確認できます。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="viewModel" type="com.nagochi.ViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.nagochi.BindingListView
            android:id="@+id/list_item"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:list="@{viewModel.list}" />
    </LinearLayout>
</layout>

MainActivity.java

package com.nagochi;

import android.databinding.DataBindingUtil;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import com.nagochi.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActivityMainBinding binding =
                DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setViewModel(new ViewModel());
    }
}

ViewModel.java

package com.nagochi;

import android.databinding.ObservableArrayList;
import android.os.Handler;
import android.os.Looper;

public class ViewModel {
    public final ObservableArrayList<String> list = new ObservableArrayList<>();

    public ViewModel() {
        list.add("データ1");
        list.add("データ2");
        list.add("データ3");

        timer();
    }

    private void timer() {
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            list.add("追加データ");
            timer();
        }, 5000);
    }
}

BindingListView.java

package com.nagochi;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import java.util.ArrayList;

public class BindingListView extends ListView {
    private ArrayAdapter<String> adapter;

    public BindingListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setList(ArrayList<String> list) {
        if (adapter == null) {
            adapter = new ArrayAdapter<>(
                    getContext(), android.R.layout.simple_list_item_1, list);
            setAdapter(adapter);
        }

        adapter.notifyDataSetChanged();
    }
}

f:id:nagochi:20170322184449p:plain