Android DataBinding 〜ListViewで使う〜


## はじめに
リスト表示でDataBindingを使う方法についてまとめます。
ListViewで一覧を表示するだけでなく、追加や削除した結果を動的に反映する方法も紹介します。
 
## Step0. 準備
例として、日時の文字列の一覧を表示するサンプルを作ります。

生成されたときの日時を文字列で表示します。

```DateTime.java
public class DateTime {
    private String dateTime;

    public DateTime() {
        this.dateTime = new Date().toString();
    }

    public String getName() {
        return dateTime;
    }
}
```

```view_row_date_time_list.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="dateTime" type="com.example.databindinglistview.DateTime" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp"
        >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:text="@{dateTime.name}"
            />
    </RelativeLayout>
</layout>
```

## Step1. Adapter作成
リストの子要素を管理するAdapterを作成します。
Viewとの紐づけにDataBindingを用いることでViewHolderが不要になり、以下のようにかなりスッキリ書けます。

```DateTimeAdapter.java
public class DateTimeAdapter extends ArrayAdapter<DateTime>{

    public DateTimeAdapter(Context context, ArrayList<DateTime> dateTimeList) {
        super(context, 0, dateTimeList);
    }

    @NonNull
    @Override
    public View getView(final int position, View convertView, @NonNull ViewGroup parent) {
        ViewRowDateTimeListBinding binding;

        if (convertView == null) {
            LayoutInflater inflater = LayoutInflater.from(getContext());
            binding = DataBindingUtil.inflate(inflater, R.layout.view_row_date_time_list, parent, false);

            convertView = binding.getRoot();
            convertView.setTag(binding);
        } else {
            binding = (ViewRowDateTimeListBinding) convertView.getTag();
        }

        binding.setDateTime(getItem(position));

        return binding.getRoot();
    }
}
```

## Step2. リストデータをセットする

xml上でList<DateTime>を渡すために、ListViewを拡張します。
```setList(...)```を新たに追加して、中でListからAdapterを作っているのですが、この```setList```は自動セッターという仕組みでxmlから```list```属性として見えるようになります。
https://developer.android.com/topic/libraries/data-binding/index.html?hl=ja#automatic_setters

```DateTimeListView.java
public class DateTimeListView extends ListView{

    DateTimeAdapter adapter;

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

    public void setList(List<DateTime> dateTimeList){
        Log.d("DEBUG", "setList is called");

        if (getAdapter() == null) {
            adapter = new DateTimeAdapter(getContext(), dateTimeList);
            setAdapter(adapter);
        }
    }
}
```

(参考)ListViewをいじらず、BindingAdapterでカスタムセッターを用意する手段もアリです。

```DataBindingAdapters.java
@BindingAdapter("list")
public static void setList(ListView listView, List<DateTime> dateTimeList) {
    Log.d("DEBUG", "setList is called");

    if (listView.getAdapter() == null) {
        DateTimeAdapter adapter = new DateTimeAdapter(listView.getContext(), dateTimeList);
        listView.setAdapter(adapter);
    }
}
```

拡張したListViewをActivityのレイアウトに配置して、先程定義した```list```属性にリストデータを入れます。

```activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <import type="java.util.List" />
        <import type="com.example.databindinglistview.DateTime"/>
        <variable name="dateTimeList" type="List&lt;DateTime&gt;" />
    </data>

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

        <com.example.databindinglistview.DateTimeListView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fadeScrollbars="false"
            app:list="@{dateTimeList}"
            />
    </LinearLayout>
</layout>
```

DataBindingでList<>を渡すときは、エスケープシーケンスを使って```List&lt;DateTime&gt;```のように書きます。
あとは、ActivityでBindingにデータをセットすればListViewに表示されます。

```MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

    List<DateTime> dateTimeList = new ArrayList<>();
    dateTimeList.add(new DateTime());
    dateTimeList.add(new DateTime());
    dateTimeList.add(new DateTime());

    binding.setDateTimeList(dateTimeList);
}
```

## Step3. アイテムのクリックイベントを拾う
イベントを受け取るHandlerを用意します。
引数は```AdapterView.OnItemClickListener```の```onItemClick```に合わせておきます。

```DataBindingAdapters.java
public interface MainEventHandler {
    void onItemClick(AdapterView<?> parent, View view, int position, long id);
}
```

あとはListViewで以下のようにセットすれば、子要素をクリックしたときのイベントを受け取ることができます。

```activity_main.xml
<data>
  ...
  <variable name="handlers" type="MainEventHandler" />
</data>

...
<com.example.databindinglistview.DateTimeListView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fadeScrollbars="false"
    app:list="@{dateTimeList}"
    android:onItemClickListener="@{handlers.onItemClick}"
    />
```

## Step4. リストの追加や削除を動的に反映させる
上記で単にリストに要素を表示するだけのものができました。
しかし、このままではリストに追加や削除があっても反映されません。

そこで今回は、リストの変更を通知できるObservableArrayListを使ってみることにしました。
Adapterを直接触らずにListViewを更新できるようになるので便利です。

```MainActivity.java
//List<DateTime> dateTimeList = new ArrayList<>();
ObservableArrayList<DateTime> dateTimeList = new ObservableArrayList<>();
```

ほか、関連する部分も```List<DateTime>```から```ObservableArrayList<DateTime>```に書き換えます。
あとは、ListViewでlist更新時に```notifyDataSetChanged()```を呼んでおきます。

```DateTimeListView.java
public void setList(List<DateTime> dateTimeList){
  ...
  adapter.notifyDataSetChanged();
}
```

長くなるのでソースは割愛しますが、この置き換えだけでリストの追加削除時にDataBindingが変更を検知して、```setList(...)```、```notifyDataSetChanged()```が呼ばれ、ListViewの表示が直ちに更新されるようになります。

### リストの追加削除を含むソース全体はこちら
https://github.com/Omoti/DataBindingListView

## おわりに
ListViewでDataBindingを使うとAdapter周りのソースをかなりシンプルにすることができました。
さらに要素をObservableArrayList<>にすることで、元のデータを操作するだけでListViewが自動的に更新されるようになり、呼び出し側はAdapterさえ触らずに済むようにできました。