Android
Kotlin

AndroidのListViewやRecyclerViewの、ViewHolderやDataBindingを調べた記録

AndroidのListViewやRecyclerViewで、ViewHolderやDataBindingを調べて一回混乱しつつも腑に落ちる所まで来たので、纏めておきます。

1週間前の自分に伝えたいこと

  • ListViewのAdapterに出てくるViewHolderと、RecyclerViewのAdapterに出てくるViewHolderは、似てるけど違うもの
    • ListViewの方は、こうやった方が高速だよというパターン
    • RecyclerViewの方は、親クラスが用意されている実装
  • いきなりDataBindingをやろうとしてソースを検索すると混乱するから、順番を追っていこう
    • 素のListView
    • ↑+ViewHolder
    • ↑+DataBinding
    • 素のRecyclderView ←ここですでにViewHolderが組み込まれている
    • ↑+DataBinding

素のListView

素のListViewはこんな感じ。AdapterのgetView()で毎回getViewById()が動く。これでも動くけどパフォーマンスが悪い。

名称未設定.png

    class ListAdapter(context: Context, val list:List<String>): ArrayAdapter<String>(context,0,list){
        private val layoutInflater = LayoutInflater.from(context)
        override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
            val itemView = layoutInflater.inflate(R.layout.cell_text,parent,false)
            val textView = itemView.findViewById(R.id.textView) as TextView
            textView.text = getItem(position)
            return itemView
        }
    }

ListView + ViewHolderパターン

getView()で毎回findViewById()をすると遅い。これを避けるために、GoogleがViewHolderというパターンを紹介している
Making ListView Scrolling Smooth
アイデアとしては、findViewById()で取得した参照をViewHolderクラス(自作)に保持して、それをView.tagに格納して再利用時にはそちらを使う、というもの。
名称未設定.png

    // View.tagにセットするViewHolder
    data class ViewHolderItem(val textView:TextView)

    class ListAdapterWithViewHolder(context: Context,val list:List<String>): ArrayAdapter<String>(context,0,list){
        private val layoutInflater = LayoutInflater.from(context)
        override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
            val (viewHolder, view) = if(convertView == null) {
                // 初回時
                val itemView = layoutInflater.inflate(R.layout.cell_text,parent,false)
                val textView = itemView.findViewById(R.id.textView) as TextView
                val viewHolder = ViewHolderItem(textView)
                itemView.tag = viewHolder
                viewHolder to itemView
            } else {
                // 再利用時
                convertView.tag as ViewHolderItem to convertView
            }
            viewHolder.textView.text = getItem(position)
            return view
        }
    }

ListView + DataBinding

DataBindingをtagに入れて使い回すことで、ViewHolderパターンと同じ事ができる。そのため、「convertViewがnullならば〜、nullでないならば〜」という処理の構造は、ViewHolderパターンの時と全く同じになる。

ViewHolderパターンで自作していたクラスは、自動生成されるXXXXXBindingで代用されるため、不要となる。
また、DataBindingの元々の性質である「モデルに値をセットすると自動でViewに反映される」機能のため、子Viewへのアクセスが見えなくなる。

名称未設定.png

    class ListAdapter(context: Context, val list:List<String>): ArrayAdapter<String>(context,0,list){
        private val layoutInflater = LayoutInflater.from(context)
        override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
            val binding = if( convertView == null) {
                val binding:CellTextBinding =
                    DataBindingUtil.inflate(layoutInflater, R.layout.cell_text, parent, false)
                binding.root.tag = binding
                binding
            }else{
                convertView.tag as CellTextBinding
            }
            // モデルに値をセット→Viewに自動で反映される
            binding.text = getItem(position)
            return binding.root
        }
    }

RecyclerView

RecyelerViewは、見た目はListViewの上位互換のように思えるが、実装が全く違っている。
参考:RecyclerViewはListViewの代替ではないよねという話
自分の感じた最も大きな違いは、ListViewはViewをやり取りするのに対して、RecyclerViewはViewHolderをやり取りしている点。
RecyclerViewでのViewHolderは、RecyclerView.ViewHolderを継承したクラス。ListViewの時にはパターンだったものが、RecyclerViewでは実装に組み込まれた感じになっている。

要注意:親クラスとなるRecyclerViewの初期化には、親Viewをセットする必要がある。ここを間違っているとjava.lang.IllegalStateException: ViewHolder views must not be attached when created. Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate(..., boolean attachToRoot)というエラーで悩まされる(悩まされた)。
参考:ViewHolder views must not be attached when created

名称未設定.png

    // RecyclerView.ViewHolderを継承した自作ViewHolder
    // 親クラスの初期化には親Viewへの参照を渡し、
    // 子クラスのプロパティには子Viewへの参照を保持する
    class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView:TextView = view.findViewById(R.id.textView)
    }

    class MyRecycleAdapter(private val context: Context, val list:List<String>) : RecyclerView.Adapter<MyViewHolder>() {
        private val layoutInflater = LayoutInflater.from(context)
        // 件数を返す
        override fun getItemCount(): Int {
            return list.size
        }
        // Viewに対応するViewHolderを生成して返す
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val view = layoutInflater.inflate(R.layout.cell_text, parent, false)
            return MyViewHolder(view)
        }
        // ViewHolderを使って、Viewの更新を行う
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            holder.textView.text = list[position]
        }
    }

RecyclerView + DataBinding

DataBindingを使う場合、RecyclerView.ViewHolderを継承した自作クラスで保持するものが、ViewからXXXXBindingへと変わる
名称未設定.png

    // RecyclerView.ViewHolderを継承した自作ViewHolder
    // 親クラスの初期化にはBinding.rootで親Viewを渡し、
    // 子クラスはbindingを保持する
    class MyViewHolder(val binding: CellTextBinding) : RecyclerView.ViewHolder(binding.root)

    class MyRecycleAdapter(context: Context, val list:List<String>) : RecyclerView.Adapter<MyViewHolder>() {
        private val layoutInflater = LayoutInflater.from(context)

        override fun getItemCount(): Int {
            return list.size
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val binding:CellTextBinding = DataBindingUtil.inflate(layoutInflater,R.layout.cell_text, parent, false)
            return MyViewHolder(binding)
        }

        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            // ここではモデルに値をセットしている
            // →DataBindingにより、自動でViewに反映される
            holder.binding.text = list[position]
        }
    }