RecyclerView + DataBinding がちょっとややこしかった件
2020/01/30更新
アダプタからリストを分離した。 ViewModel+DataBinding+RecyclerViewの実装ならこっちのがいいかも↓
参考
基本自分向けなのでやたら長いです。
- 環境
- Android Studio 3.2.1
- Kotlin
2018-01-12更新: Adapter で保持するリストをMutableList
に修正
やったこと
- RecyclerView の
adapter
要素にアダプタをバインドする - 展開する項目を DataBinding で実装し、イベントやコンテンツを制御する
- DiffUtil で Adapter のリストの差分検知を実装する
用語的に合っているのかわからない←
DiffUtil はリスト更新時の差分検知用で、今回はあまり深くは触れません。詳しく知りたい場合はこちらへ。
実際の動きはこんな感じです。
レイアウトファイルの作成
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 楽しい。