日々是好日

プログラミングについてのあれこれ、ムダ知識など

RecyclerView + DataBinding がちょっとややこしかった件

2020/01/30更新

アダプタからリストを分離した。 ViewModel+DataBinding+RecyclerViewの実装ならこっちのがいいかも↓

kcpoipoi.hatenablog.com


参考

qiita.com

medium.com

基本自分向けなのでやたら長いです。

2018-01-12更新: Adapter で保持するリストをMutableListに修正

やったこと

  • RecyclerView のadapter要素にアダプタをバインドする
  • 展開する項目を DataBinding で実装し、イベントやコンテンツを制御する
  • DiffUtil で Adapter のリストの差分検知を実装する

用語的に合っているのかわからない←

DiffUtil はリスト更新時の差分検知用で、今回はあまり深くは触れません。詳しく知りたい場合はこちらへ。

実際の動きはこんな感じです。

f:id:kcpoipoi:20190110235344g:plain:w300

レイアウトファイルの作成

RecyclerView の配置

今回は Fragment 上に RecyclerView を配置します。

RecordAdapter と MainViewModel をバインドします。

<layout ...>
    <data>
        <variable name="adapter" type="RecordAdapter"/>
        <variable name="viewmodel" type="MainViewModel" />
    </data>
    <android.support.constraint.ConstraintLayout ...>
        <android.support.v7.widget.RecyclerView
                android:id="@+id/recycler"
                android:adapter="@{adapter}"
                bind:viewmodels="@{viewmodel.recordModels}"
                />
    </android.support.constraint.ConstraintLayout>
</layout>

カスタムセッターbind:viewmodelsは次のように@BindingAdapterで定義しました。

@BindingAdapter("bind:viewmodels")
fun RecyclerView.setViewModels(recordModels: List<RecordModel>?){
    if (recordModels != null){
        val adapter = this.adapter as RecordAdapter
        val diff = DiffUtil.calculateDiff(RecordAdapter.Callback(adapter.recordModels, recordModels), true)
        adapter.recordModels.let {
            it.clear()
            it.addAll(recordModels)
        }
        diff.dispatchUpdatesTo(adapter)
    }
}

RecordModel の定義は後述です。

アイテムのレイアウト定義

RecyclerView に表示するアイテムのレイアウトです。

<layout ...>
    <data>
        <variable name="viewmodel" type="RecordModel" />
    </data>

    <android.support.v7.widget.CardView ...>
        <android.support.constraint.ConstraintLayout ...>
            <ImageView
                    android:id="@+id/image"
                    bind:image_as_rect="@{viewmodel.record.imageUrl}" .../>
            <TextView
                    android:id="@+id/date"
                    android:text="@{viewmodel.record.recordedTime}" .../>
            <TextView
                    android:id="@+id/comment"
                    android:text="@{viewmodel.record.comment}" .../>
        </android.support.constraint.ConstraintLayout>
    </android.support.v7.widget.CardView>
</layout>

RecordModel の定義は後述です。

RecordModel の定義

アイテムにバインドする RecordModel を定義します。

MainNavigator インターフェースは……Androidアプリ設計パターン入門をお読みください(;˘ω˘)

ちなみに実体は MainActivity (AppCompatActivity) です。

class RecordModel(val record: MyRecord) {
    private var navigator: MainNavigator? = null
    ...
    fun itemClick(record: MyRecord) {
        navigator?.onOpenImage(record)
    }

    fun setNavigator(navigator: MainNavigator?){
        this.navigator = navigator
    }
}

//単純なデータクラス
data class MyRecord(private val recordJSON: JSONObject){
    val imageUrl: String = recordJSON.getString("image_url")
    val comment: String = recordJSON.getString("comment")
    val recipeType: String = recordJSON.getString("recipe_type")
    val recordedTime: String = recordJSON.getString("recorded_at")
}

ここまででレイアウト関係の定義は終わりです。

処理の実装

MainViewModel の実装

Adapter にバインドするためのリストをMutableLiveData<List<RecordModel>>として保持します。

MainViewModel#setRecordsがコールされるとーー

recordModels.value が更新

bind:viewmodels="@{viewmodel.recordModels}" でバインドされているので、拡張関数 RecyclerView#setViewModels がコール

Adapter のリスト更新

という風に処理が流れます。

class MainViewModel(application: Application): AndroidViewModel(application){
    ...
    val recordModels = MutableLiveData<List<RecordModel>>()

    fun setRecords(records: List<MyRecord>){
        ...
        recordModels.value = records
    }
    ...
}

RecyclerView.Adapter の実装

RecyclerView.Adapter<T>を継承したRecordAdapterと、ネストクラスBindingHolderを定義します。全体的には RecyclerView の実装と同じです。

ポイントは、保持するリストをList<RecordModel>としているところでしょうか。RecordModel はコンストラクタでval record: MyRecordとしているので、recordModel.recordといった形で MyRecord を参照できます。

class RecordAdapter : RecyclerView.Adapter<RecordAdapter.BindingHolder>() {
    //中身はDataBinding機構を介してMainViewModelで保持
    //2018-01-12更新: val, MutableListで定義
    val recordModels: MutableList<RecordModel> = mutableListOf()

    // Viewの生成 (invoked by layout manager)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = RecordItemBinding.inflate(inflater, parent, false)
        return BindingHolder(binding)
    }

    override fun getItemCount(): Int {
        return recordModels.count()
    }

    // Replace the contents of a view (invoked by the layout manager)
    override fun onBindViewHolder(holder: BindingHolder, position: Int) {
        val recordModel = recordModels[position]
        holder.binding.viewmodel = recordModel

        //ClickListenerのセットはココ!
        holder.binding.parentLayout.setOnClickListener{
            //処理はRecordModel#itemClickに実装
            recordModel.itemClick(recordModel.record)
        }
        ...
        holder.binding.executePendingBindings()
    }

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder.
    class BindingHolder(var binding: RecordItemBinding): RecyclerView.ViewHolder(binding.root)
    ...
}

今後のためにほぼ全部書いてしまいます。ほんとは GitHub とかにプッシュしたいんですが、諸事情により無理……。

onBindingViewHolderにおいて ClickListener を実装していますが、処理の実装は RecordModel に委譲しています。

RecyclerView の初期化

最後にFragment#onCreateViewにて RecyclerView を初期化します。

これはそんなに重要でもないのでサラッと。

//onCreateViewからコール
private fun setupRecyclerView(){
    val layoutManager = when (resources.configuration.orientation) {
        Configuration.ORIENTATION_PORTRAIT -> {
            GridLayoutManager(context, 2)
        }
        Configuration.ORIENTATION_LANDSCAPE -> {
            LinearLayoutManager(context)
        } else -> throw NotImplementedError()
    }

    binding.recycler.layoutManager = layoutManager
    binding.adapter = RecordAdapter()
    ...
}

これで実装は完了です。うーん複雑(に見える)。

所感

やはり RecyclerView に保持するアイテムのリストをList<RecordModel>にしているところがポイントですかねー。

最初に実装したときはList<MyRecord>としていましたが、「イベントとかちゃんと処理したいし MVVM っぽく実装してぇ!!」というこだわりが発動してしまい、このような形になりました。

RecordModel にすることでクリックなどのアクションの実装を Adapter から除外し、RecordModel でまとめて実装できるため見通しがよくなったと思います。

もう少しこだわるとすれば、RecordModel にインターフェースを定義することで、独自リスナーも実装できるようになります。

MVVM + DataBinding 楽しい。