RecyclerViewにLiveDataをDataBindingしたい
状況
- List型のデータを取得し、画面にRecyclerViewで表示させたい。
- だけどobserve書きたくないし、RecyclerViewのAdapterクラスをViewModelで持ちたくないし。
- LiveDataに値をpostしたらいい感じに紐づいてくれないだろうか
実装
以下のように実装した。 主に4つのクラスで構成される。
- ViewModel
- Fragment
- RecyclerView.Adapter
- Fragmentのxml
ViewModel
/** * メイン画面のViewModel */ class MainViewModel constructor(private val showNovelListUseCase: ShowNovelListUseCase) : ViewModel() { // 小説リスト val novelList = MutableLiveData<List<NovelIntroduction>>() // onCreate時に呼び出す想定 fun onCreate() = GlobalScope.launch { val novel = showNovelListUseCase.getNovelList("") novelList.postValue(novel) } // ViewModel()にDIする場合はFactoryクラスを作成する必要がある。 class Factory(private val showNovelListUseCase: ShowNovelListUseCase) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(showNovelListUseCase) as T } } }
Fragment
class MainFragment : Fragment() { companion object { fun newInstance() = MainFragment() } // ViewModel private lateinit var viewModel: MainViewModel // binding private lateinit var binding: MainFragmentBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { // レイアウトをinflateする binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false) // LiveDataとxmlをbindする binding.setLifecycleOwner(this) // inflateしたViewを返却する return binding.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) // viewModel作成 viewModel = ViewModelProviders .of(this, MainViewModel.Factory(ShowNovelListUseCase())) .get(MainViewModel::class.java) // 生成したviewModelをbindする binding.viewModel = viewModel // 横向き val manager = LinearLayoutManager(context) manager.orientation = LinearLayoutManager.HORIZONTAL binding.recyclerView.layoutManager = manager binding.recyclerView.adapter = NovelListAdapter() viewModel.onCreate() } }
RecyclerView.Adapter
class NovelListAdapter : RecyclerView.Adapter<NovelListAdapter.ViewHolder>() { // 表示アイテム private var novelList: List<NovelIntroduction> = emptyList() override fun onBindViewHolder(holder: ViewHolder, position: Int) { if (holder is ItemViewHolder && novelList.size > position) { holder.bind(novelList[position]) } } override fun getItemCount(): Int { return novelList.size } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelListAdapter.ViewHolder { return ItemViewHolder(parent) } /** * データをセットしてアップデートする */ fun update(novelList: List<NovelIntroduction>) { this.novelList = novelList notifyDataSetChanged() } abstract class ViewHolder(view: View) : RecyclerView.ViewHolder(view) class ItemViewHolder( private val parent: ViewGroup, private val binding: MainItemBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.main_item, parent, false ) ) : ViewHolder(binding.root) { fun bind(item: NovelIntroduction) { binding.item = item } } companion object { @JvmStatic @BindingAdapter("items") fun RecyclerView.bindItems(items: List<NovelIntroduction>?) { // まだ情報が取得できていない場合はitemsがnullになる可能性があるため、nullチェック必須。 if (items == null) { return } // RecyclerView.Adapterを継承しているので、RecyclerViewに設定されているadapterを取得できる val adapter = adapter as NovelListAdapter adapter.update(items) } } }
main_fragment.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="viewModel" type="c.offwhite.sampledi.ui.main.MainViewModel"/> </data> <android.support.constraint.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.main.MainFragment"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" app:items="@{viewModel.novelList}"/> </android.support.constraint.ConstraintLayout> </layout>
解説
まずはLiveDataの設定。ここは通常のLiveDataのコーディングと同じである。 showNovelListUseCase.getNoveList()は通信が発生する可能性があるのでバックグラウンドでの実行とし、LiveDataはpostValueで値を設定する
ViewModel
// 小説リスト val novelList = MutableLiveData<List<NovelIntroduction>>() // onCreate時に呼び出す想定 fun onCreate() = GlobalScope.launch { val novel = showNovelListUseCase.getNovelList("") novelList.postValue(novel) }
次にxmlのRecyclerViewにDataBindingを追加する。app:items="@{viewModel.novelList}"の行がそれにあたる。
main_fragment.xml
<android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" app:items="@{viewModel.novelList}"/>
しかし、本来RecyclerViewにList
自作セッターの定義方法は@BindingAdapter("対象のattribute")を指定することで可能。今回はapp:itemsに対する自作セッターのため、@BindingAdapter("items")となる。
なお、staticメソッドでないとダメな模様。
RecyclerView.Adapter
companion object { @JvmStatic @BindingAdapter("items") fun RecyclerView.bindItems(items: List<NovelIntroduction>?) { // まだ情報が取得できていない場合はitemsがnullになる可能性があるため、nullチェック必須。 if (items == null) { return } // RecyclerView.Adapterを継承しているので、RecyclerViewに設定されているadapterを取得できる val adapter = adapter as NovelListAdapter adapter.update(items) } }
これでViewModel.novelListが更新されるたびに adapter.update()が呼び出されるため、adapter内のデータ変更が通知される。
RecyclerView.Adapter
/** * データをセットしてアップデートする */ fun update(novelList: List<NovelIntroduction>) { this.novelList = novelList notifyDataSetChanged() }
はまったところ
Android DataBinding error. Could not find accessorが発生
- apply plugin: 'kotlin-kapt'がないと@BindingAdapterが動かない。しかしDataBindingはなくても大丈夫という罠。
Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter itemsが発生
- @BindingAdapter(items)を指定していたRecyclerView.bindItemsのパラメータをnon-nullにしていたが、非同期処理中にnullのLiveDataをbindしてしまい、non-nullを違反してしまっていた。nullableにしてnullチェックを導入することで回避している。