24
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RecyclerViewとDatabindingとDiffUtilと。

Posted at

やりたいこと

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

サンプルアプリの仕様

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

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

sample.gif

実装

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

MainActivity

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

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()
    }
}
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のクリックイベントもここで受け取っています。

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に設定しているだけです

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)
    }
}
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自体は特筆すべきところはありません。

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に記述しました。

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で使用します。

@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の更新がうまく実行されませんでした。

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

24
18
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?