TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

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チェックを導入することで回避している。