DataBinding で ListViewを表示

# はじめに
DataBindingとListViewを使って簡単なリスト表示をやってみたときのメモ。

# やったこと
## 1. DataBindingの有効化
Gradle(app)に設定を追記。

```gradle:app/build.gradle
android {
    dataBinding {
        enabled true
    }
}
```

## 2. Modelの作成
ListViewに表示するデータを保持、整形するためのModelを作成。

```kotlin:model/Person.kt
class Person(
    private val firstName: String,
    private val lastName: String,
    private val age: Int
) {
    fun getFullName() = "$firstName $lastName"
    fun getDisplayAge() = "$age 歳"
}
```

![Untitled Diagram (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472695/48925cd7-5539-10ba-dd23-b01fca705fe8.png)

## 3. ViewModelの作成
ModelとViewをつなぎ合わせるためのViewModelを作成。
ほとんどの属性値、メソッドはビューとのバインド用。

```kotlin:viewmodel/MainViewModel.kt
class MainViewModel : ViewModel() {
    private val dataList by lazy { ArrayList<Person>() }
    private val _items = MutableLiveData<List<Person>>()
    val items: LiveData<List<Person>> = _items
    var firstName = MutableLiveData("")
    var lastName = MutableLiveData("")
    var age = MutableLiveData("")
    var isEnabled = MediatorLiveData<Boolean>()

    private fun isAllEntered() =
        firstName.value!!.isNotBlank() &&
        lastName.value!!.isNotBlank() &&
        age.value!!.isNotBlank()

    init {
        isEnabled.addSource(firstName) { isEnabled.postValue(isAllEntered()) }
        isEnabled.addSource(lastName) { isEnabled.postValue(isAllEntered()) }
        isEnabled.addSource(age) { isEnabled.postValue(isAllEntered()) }
    }

    fun onAddClick() {
        val fnVal = firstName.value ?: ""
        val lnVal = lastName.value ?: ""
        val ageVal = if (age.value!!.isEmpty()) 0 else Integer.parseInt(age.value!!)
        dataList.add(Person(fnVal, lnVal, ageVal))
        _items.value = dataList
    }

    fun getOnItemClickListener(): AdapterView.OnItemClickListener {
        return AdapterView.OnItemClickListener { adapterView, view, i, l ->
            dataList.removeAt(i)
            _items.value = dataList
        }
    }
}
```

## 4. カスタムバインディングアダプタの作成
カスタムバインディングアダプタを作成することで、
レイアウトファイルに対してカスタム属性とメソッドをバインドさせることができる。

```kotlin:util/DataBindAdapter.kt
@BindingAdapter("onItemClickListener")
fun setOnItemClickListener(listView: ListView, listener: AdapterView.OnItemClickListener) {
    listView.onItemClickListener = listener
}
```

メソッドに対して`@BindingAdapter(属性)`を付与することによって
レイアウトファイルに任意の属性が設定でき、バインドができる。

```xml
<ListView
app:onItemClickListener="@{viewModel.getOnItemClickListener()}">
</ListView>
```

属性値である`viewModel.getOnItemClickListener()`が
`setOnItemClickListener`の第2引数に渡される。
ここでは、ListViewに対してクリック時のリスナーを登録。

## 5. レイアウトファイルの作成
例によってデータバインディング用のレイアウトに変換したのち、
バインドさせたい属性とViewModelの属性値を
`@{~}`(単方向)、`@={~}`(双方向)でバインドさせていく。

***・メインアクティビティ***

```xml:layout/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.example.mymvvmlistview.viewmodel.MainViewModel" />
    </data>

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

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/etLastName"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="3.0"
                android:text="@={viewModel.lastName}"
                android:ems="10"
                android:hint="苗字"
                android:inputType="textPersonName"
                android:autofillHints="" />

            <EditText
                android:id="@+id/etFirstName"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="3.0"
                android:text="@={viewModel.firstName}"
                android:ems="10"
                android:hint="名前"
                android:inputType="textPersonName"
                android:autofillHints="" />

            <EditText
                android:id="@+id/editText3"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="2"
                android:text="@={viewModel.age}"
                android:ems="10"
                android:hint="年齢"
                android:inputType="number"
                android:autofillHints="" />

            <Button
                android:id="@+id/btAdd"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="2"
                android:onClick="@{() -> viewModel.onAddClick()}"
                android:text="追加"
                android:enabled="@{viewModel.isEnabled()}"/>
        </LinearLayout>
        <ListView
            android:id="@+id/lvItem"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            app:onItemClickListener="@{viewModel.getOnItemClickListener()}">
        </ListView>
    </LinearLayout>
</layout>
```

***・ListView用のカスタムセル***

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

    <data>
        <variable
            name="person"
            type="com.example.mymvvmlistview.model.Person" />
        <variable
            name="viewModel"
            type="com.example.mymvvmlistview.viewmodel.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp">
        <TextView
            android:id="@+id/tvName"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginStart="30dp"
            android:layout_weight="0.7"
            android:gravity="center"
            android:textAlignment="center"
            android:textSize="18sp"
            tools:text="@{person.fullName}" />

        <TextView
            android:id="@+id/tvAge"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginEnd="30dp"
            android:layout_weight="0.3"
            android:gravity="center"
            android:textAlignment="center"
            android:textSize="18sp"
            tools:text="@{person.displayAge}" />
    </LinearLayout>
</layout>
```

## 6. ListViewのアダプタを作成
ListViewで使用するカスタムアダプタを作成する。

```kotlin:MyCustomAdapter.kt
class MyCustomAdapter(
    private val context: Context,
    private var dataList: List<Person>
) : BaseAdapter() {
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val binding =
            if (convertView == null) {
                val inflater = LayoutInflater.from(context)
                // https://developer.android.com/topic/libraries/data-binding/expressions?hl=ja
                val tBinding: ListItemCellBinding =
                    ListItemCellBinding.inflate(inflater, parent, false)
                // tagにインスタンスをセット(convertViewが存在する場合に使い回すため)
                tBinding.root.tag = tBinding
                tBinding
            } else {
                convertView.tag as ListItemCellBinding
            }

        binding.person = getItem(position) as Person
        // 即時バインド
        binding.executePendingBindings()

        return binding.root
    }

    fun updateItems(newItems: List<Person>) {
        dataList = newItems
        // 変更の通知
        notifyDataSetChanged()
    }

    override fun getItem(position: Int): Any {
        return dataList[position]
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getCount(): Int {
        return dataList.size
    }
}
```

## 7. Activityの修正

```kotlin:ui/MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val mViewModel: MainViewModel by lazy {
        ViewModelProvider(this).get(MainViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
            this, R.layout.activity_main
        )

        with(binding) {
            viewModel = mViewModel
            lvItem.adapter = MyCustomAdapter(applicationContext, ArrayList(0))
            lifecycleOwner = this@MainActivity
            (viewModel as MainViewModel).items.observe(this@MainActivity, Observer { item ->
                val adapter = lvItem.adapter as MyCustomAdapter
                adapter.updateItems(item)
            })
        }
    }
}
```

## 8. 動作確認
双方向バインディングにより、画面上の「苗字」「名前」「年齢」の値を監視し、
なにかしらの入力値が入るまで追加ボタンを押すことができないようにした。

<img width="320" alt="スクリーンショット 2020-04-04 16.48.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472695/87fc5ecb-f4a2-1607-ec49-5ae8127d3468.png">

<img width="320" alt="スクリーンショット 2020-04-04 17.30.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472695/8913bfb5-2567-b860-713f-2a11706436ba.png">

<img width="320" alt="dg4r2-hb6b9.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472695/5b6a0ebe-52f4-15e5-def8-749ea699c30c.gif">

## 参考
先人の知恵をお借りしました。
ありがとうございます。:bow_tone1:
[Android DataBinding 〜ListViewで使う〜](https://qiita.com/Omoti/items/537d9a86b4a5e95b35ed)
[【Android】LiveData+DataBinding+ViewModelでListView作成](https://www.itcowork.co.jp/blog/?p=5223)
[How can I use Android DataBinding in a listview and still use a ViewHolder pattern?](https://stackoverflow.com/questions/40557087/how-can-i-use-android-databinding-in-a-listview-and-still-use-a-viewholder-patte)