RecyclerViewとDatabindingとDiffUtilと。

# やりたいこと
RecyclerViewのデータ更新をnotifyDataSetChangedとかを使わずに、楽してやりたい。。

## サンプルアプリの仕様

話を単純化するためにシンプルなものを作ります。

- Addボタンを押すとリストが追加される
- アイテムをタップすると削除される


![sample.gif](https://qiita-image-store.s3.amazonaws.com/0/202533/98a87791-7d11-ef29-ba92-5a1cc906c9ea.gif)


## 実装

基本的にはMVVMを意識した設計になっています。
登場人物はMainActivityとそれに紐づいたMainViewModel、
Adapterと各RecyclerView.ViewHolderに紐づいたItemViewModel、そしてDiffUtil.Callbackです。

### MainActivity

ボタンとRecyclerViewだけを定義したシンプルな画面です。
MainViewModelのbindingをやっています。
RecyclerViewの `app:viewModels="@{viewModel.items}"` はBindingAdapterを定義しています。ここの実装は後述します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding?.viewModel = MainViewModel()
        binding?.adapter = ItemAdapter()
    }
}
```

```xml: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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="adapter"
            type="com.sample.recyclerviewsample.adapter.ItemAdapter" />

        <variable
            name="viewModel"
            type="com.sample.recyclerviewsample.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:adapter="@{adapter}"
            app:layoutManager="android.support.v7.widget.LinearLayoutManager"
            app:viewModels="@{viewModel.items}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.onClickAddButton()}"
            android:text="ADD" />

    </LinearLayout>
</layout>
```

次はMainActivity用のViewModelです。RecyclerViewで表示するアイテムリストを保持しています。
ここで大事なのはObservableArrayListでadapterで使用するリストを管理していることです。これをBindingAdapterを使ってRecyclerViewにセットしています。
また、今回はとりあえずViewHolderのクリックイベントもここで受け取っています。

```kotlin:MainViewModel.kt
class MainViewModel: ItemViewModel.Listener {

    val items: ObservableArrayList<ItemViewModel> = ObservableArrayList()

    fun onClickAddButton() {
        // クリックイベントを受け取ったらitemsにitemを追加します
        items.add(ItemViewModel("No.${items.size}", items.size, this))
    }

    // itemをクリックされたら、クリックされたアイテムを削除します
    override fun onClick(id: Int) {
        val target = items.find { it.id == id }
        items.remove(target)
    }
}
```

### リストアイテム

ここはViewModelのtitleを素直にViewHolderに設定しているだけです

```kotlin:ItemViewHolder.kt
class ItemViewModel(val title: String = "",
                    val id: Int = 0,
                    private val listener: Listener) {

    fun onClick() = listener.onClick(id)

    interface Listener {
        fun onClick(id: Int)
    }
}
```

```xml:row_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.shcahill.recyclerviewsample.adapter.ItemViewModel" />
    </data>
    
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="@{() -> viewModel.onClick()}"
        android:text="@{viewModel.title}" />
</layout>
```


### Adapter

Adapter自体は特筆すべきところはありません。

```kotlin:ItemAdapter.kt
class ItemAdapter : Adapter<ItemAdapter.ViewHolder>() {
    var items: List<ItemViewModel> = listOf()

    override fun getItemCount(): Int = items.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = RowItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.binding.viewModel = items[position]
    }

    class ViewHolder(val binding: RowItemBinding) : RecyclerView.ViewHolder(binding.root)
}
```

### BindingAdapter and DiffUtil

ここからが大事なところです。
DiffUtilで使用するDiffUtil.Callbackを下記のように定義しました。
記述場所はどこでもよいですが、私はAdapterに記述しました。

```kotlin
class Callback(private val old: List<ItemViewModel>, 
               private val new: List<ItemViewModel>) : DiffUtil.Callback() {
    override fun getOldListSize(): Int = old.size
    override fun getNewListSize(): Int = new.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
            old[oldItemPosition].id == new[newItemPosition].id


    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
            old[oldItemPosition].title == new[newItemPosition].title
}
```

oldとnewのリストを受け取り、差分のチェックを行うコールバックです。オブジェクト自体が変わっている場合は`areItemsTheSame` でfalseが返るように実装します。また、オブジェクトは同じでも、中のコンテンツが変更されている場合は`areContentsTheSame` でfalseが返るように実装します。
このCallbackは次のBindingAdapterで使用します。

```kotlin
@BindingAdapter("viewModels")
fun setViewModels(recyclerView: RecyclerView, items: ObservableArrayList<ItemViewModel>) {
    val adapter = recyclerView.adapter as ItemAdapter
    val diff = DiffUtil.calculateDiff(Callback(adapter.items, items), true)
    // ここでListに変換してあげないとdispatchUpdatesToをかけれない
    adapter.items = items.toList()
    diff.dispatchUpdatesTo(adapter)
}
```

BindingAdapterでRecyclerViewにViewModelのリストをセットする口を用意します。RecyclerViewはObservableArrayListで受け取っているため、リストの更新があるとここに処理が飛んできます。その際、adapterが保持しているリストと、新しく受け取るリストの差分を先ほどのCallbackでチェックします。その結果が`diff` です。
このdiffに対して`dispatchUpdatesTo` を実行してあげることでadapterの最適なnotify系のメソッドがコールされます。
ここで、注意すべきことは、引数で渡ってきたitemsはObservableArrayListのため、toListで単純なリストに変換してあげる必要があるようです。これをやらないと、DiffUtilをかける前に、勝手にadapterのリストが更新されてしまうため、RecyclerViewの更新がうまく実行されませんでした。

自分のやり方がマズいのかもしれませんが、もっとシンプルなやり方があればご指摘いただけると幸いです。