Posted at

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]
}
}