LoginSignup
5
3

More than 5 years have passed since last update.

RecyclerView に ObservableArrayList を DataBinding して変更通知で更新する

Posted at

Databinding使っているときのRecyclerView

Databindingを使っていてリスト表示に困ったことありませんか?

弊社ではjava + butterknifeからkotlin + databindingに移行しています。
そこで不思議に思ったのがRecyclerViewに変更通知して自動更新してもらう仕組みがないというところです(どなたか知ってたら教えてください)。
ListViewのサンプルは見つかったんですが、recyclerviewのは中々見つからず。

実装

というわけで実装してみました。

RecyclerAdapterです。

ObservableRecyclerAdapter.kt
abstract class ObservableRecyclerAdapter(activity: FragmentActivity,
                                         recyclerItems: ObservableArrayList<out CustomRecyclerItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    protected val items: MutableList<CustomRecyclerItem> = mutableListOf()
    private val progressBar: RecyclerProgressBar = RecyclerProgressBar()
    protected val layoutInflater: LayoutInflater = LayoutInflater.from(activity)

    init {
        items.addAll(recyclerItems.toList())

        // Bind ObservableList
        recyclerItems.addOnListChangedCallback(object : ObservableList.OnListChangedCallback<ObservableList<CustomRecyclerItem>>() {
            override fun onChanged(sender: ObservableList<CustomRecyclerItem>?) {
                sender?.let {
                    items.clear()
                    items.addAll(it.toList())
                }
                notifyDataSetChanged()
            }

            override fun onItemRangeRemoved(sender: ObservableList<CustomRecyclerItem>?, positionStart: Int, itemCount: Int) {
                for (index in 0..(itemCount - 1)) {
                    items.removeAt(positionStart)
                }
                notifyItemRangeRemoved(positionStart, itemCount)
            }

            override fun onItemRangeMoved(sender: ObservableList<CustomRecyclerItem>?, fromPosition: Int, toPosition: Int, itemCount: Int) {
                val moveItems = items.subList(fromPosition, fromPosition + (itemCount - 1))
                moveItems.map { items.remove(it) }
                moveItems.forEachIndexed { index, customRecyclerItem ->
                    items.add(toPosition + index, customRecyclerItem)
                }
                notifyDataSetChanged()
            }

            override fun onItemRangeInserted(sender: ObservableList<CustomRecyclerItem>?, positionStart: Int, itemCount: Int) {
                val insertItems = sender?.toList() ?: return
                items.addAll(positionStart, insertItems.subList(positionStart, positionStart + itemCount))
                notifyItemRangeInserted(positionStart, itemCount)
            }

            override fun onItemRangeChanged(sender: ObservableList<CustomRecyclerItem>?, positionStart: Int, itemCount: Int) {
                val changedItems = sender?.toList() ?: return
                changedItems.subList(positionStart, positionStart + itemCount).forEachIndexed { index, customRecyclerItem ->
                    items[positionStart + index] = customRecyclerItem
                }
                notifyItemRangeChanged(positionStart, itemCount)
            }
        })
    }

    override fun getItemViewType(position: Int): Int {
        return items[position].viewType()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            RecyclerItemViewType.PROGRESS_BAR -> {
                // TODO: Databindingにする
                ProgressBarViewHolder(layoutInflater.inflate(R.layout.recycler_item_progress_bar, parent, false))
            }
            else -> {
                wrappedOnCreateViewHolder(parent, viewType)
            }
        }
    }

    abstract fun wrappedOnCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ProgressBarViewHolder -> {
            }
            else -> {
                wrappedOnBindViewHolder(holder, position)
            }
        }
    }

    abstract fun wrappedOnBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)

    fun addLoadingItem() {
        items.add(progressBar)
        notifyDataSetChanged()
    }

    fun removeLoadingItem() {
        items.remove(progressBar)
        notifyDataSetChanged()
    }
}
CustomRecyclerItem.kt
interface CustomRecyclerItem {
    fun viewType(): Int
}

コンストラクタ

ObservableRecyclerAdapter(activity: AppCompatActivity, recyclerItems: ObservableArrayList<out CustomRecyclerItem>)

recyclerItems: ObservableArrayList<out CustomRecyclerItem> で変更通知に対応したListを受け取っています。これを元に表示を行います。

override想定で作ると意外とこの辺がキモなんじゃないかと思います。ObservableArrayListのジェネリクスで苦労しました。このへんうまいことやってくれる方法あったらご教示ください。

Adapterで扱うRecyclerItem

CustomRecyclerItem を実装したクラスを渡してください。viewTypeを返すだけです。

内部に表示用のリストのコピーを持つ

protected val items: MutableList<CustomRecyclerItem> = mutableListOf()

リストのデータを取得する間はプログレスのセルを表示したかったので、単純に渡されたObservableArrayListを表示に使うとプログレスをaddした時点で意図しない変更通知が届いてしまいます。

変更通知で表示を更新する

recyclerItems.addOnListChangedCallback(
    object : ObservableList.OnListChangedCallback<ObservableList<CustomRecyclerItem>>() {
    ~~~~~~~~~
})

callbackを登録して各変更通知をクラス内に保持しているリストに反映させます。 その後、notifyして描画を更新しています。

これでリストの変更を反映するといい感じにアニメーションもしてくれます。

プログレス

fun addLoadingItem()
fun removeLoadingItem()

リストの最下部にくるくるまわるやつが出ます。

使い方

  • ObservableRecyclerAdapterを継承して使います。
MessageRecyclerAdapter.kt
class MessageRecyclerAdapter(
        private val activity: AppCompatActivity,
        recyclerItems: ObservableArrayList<MessageViewModel>) : ObservableRecyclerAdapter(activity, recyclerItems) {

    override fun wrappedOnCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            MessageType.Header.ordinal -> {
                HeaderViewHolder(activity, HeaderViewModelBinding.inflate(layoutInflater, parent, false))
            }
            MessageType.Message.ordinal -> {
                MessageViewHolder(activity, MessageViewModelBinding.inflate(layoutInflater, parent, false))
            }
            MessageType.AD.ordinal -> {
                ADViewHolder(activity, ADViewModelBinding.inflate(layoutInflater, parent, false))
            }
            else -> {
                throw Exception()
            }
        }
    }

    override fun wrappedOnBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = items[position]

        when (holder) {
            is HeaderViewHolder -> {
                holder.bind(item as HeaderViewModel)
            }
            is MessageViewHolder -> {
                holder.bind(item as MessageViewModel)
            }
            is ADViewHolder -> {
                holder.bind(item as ADViewModel)
            }
        }
    }
}

ちなみにMessageViewModelはCustomRecyclerItemを実装しています。

おわり

Just ideaで作ったものなので至らぬ部分もあるかもしれません。

なにかありましたらぜひご指摘ください。よろしくおねがいします。

5
3
0

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
5
3