【Kotlin】ListViewを簡単なモデルで理解しよう【Androidアプリ開発】
ここではAndroidのListViewを、簡単なモデルを使って実装してみる。本記事の完成イメージがこちら。
標準に搭載されているタイムゾーンの配列をTimeZone.getAvailableIDs()で取得して、一覧表示する単純な仕様となっている。APIなど複雑な処理はないのでリストビューの理解だけに集中できるように進めていく。
ListViewのレイアウトファイルを作る
配置したListViewの行のレイアウトファイルを作りたい場合は、ListViewをクリックして、listitemの項目の...をクリック。開いたダイアログの右上から、New layout Fileを選択して作成すると簡単だ。
こんな感じにレイアウトする。
リストの行のタップ領域高さは、48dp以上が推奨されているようだ。あまりに狭いと、ユーザビリティが悪くなるので気をつけよう。
基本のListView
BaseAdapterクラスを継承した、TimeZoneAdapterクラスを作成する。
class TimeZoneAdapter(private val context: Context,
private val timeZones: Array<String> = TimeZone.getAvailableIDs())
: BaseAdapter() {
private val inflater = LayoutInflater.from(context)
// インデックスp0にある行のビューを返す
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = convertView ?: inflater.inflate(R.layout.list_time_zone_row, parent, false)
val timeZoneId = getItem(position)
val timeZone = TimeZone.getTimeZone(timeZoneId)
val timeZoneLabel = view.findViewById<TextView>(R.id.timeZone)
timeZoneLabel.text = "${timeZone.displayName}(${timeZone.id})"
val clock = view.findViewById<TextClock>(R.id.clock)
clock.timeZone = timeZone.id
return view
}
// インデックスp0にあるデータを返す
override fun getItem(position: Int) = timeZones[position]
// 行を識別するためのユニーク値
override fun getItemId(position: Int) = position.toLong()
// リスト表示するデータ件数
override fun getCount() = timeZones.size
}
getViewの部分が一番混乱するかもしれない。getViewは次のような働きをする。
getViewは、positionに指定された行のViewを返す。Viewが生成されていないならinflaterで生成し、すでに存在すれば、そのViewを使い回す。また、実際の値をリストに設定する処理もここで書く。
ViewHolderで速度改善
実はさきほどのプログラムは、次の部分に問題がある。
val timeZoneLabel = view.findViewById<TextView>(R.id.timeZone)
スクロールするたびに、findViewByIdを呼び出すのはコストがかかるのだ。つまりスクロール動作が遅くなる。そこで登場するのがViewHolderパターンだ。
Viewにはtagプロパティがあり、そこに任意のオブジェクトを1つ持つことができる。これを利用して、あらかじめtimeZoneLabelなどのインスタンスを生成して持たせておくのだ。convertViewはリサイクルされるので、tagに持たせたオブジェクトも再利用されるはずである。そして次のコードが、 ViewHolderパターンに書き換えたものだ。
class TimeZoneAdapter(private val context: Context,
private val timeZones: Array<String> = TimeZone.getAvailableIDs())
: BaseAdapter() {
private val inflater = LayoutInflater.from(context)
// インデックスp0にある行のビューを返す
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = convertView ?: createView(parent)
val timeZoneId = getItem(position)
val timeZone = TimeZone.getTimeZone(timeZoneId)
val viewHolder = view.tag as ViewHolder
@SuppressLint("SetTextI18n")
viewHolder.name.text = "${timeZone.displayName}(${timeZone.id})"
viewHolder.clock.timeZone = timeZone.id
return view
}
// インデックスp0にあるデータを返す
override fun getItem(position: Int) = timeZones[position]
// 行を識別するためのユニーク値
override fun getItemId(position: Int) = position.toLong()
// リスト表示するデータ件数
override fun getCount() = timeZones.size
private fun createView(parent: ViewGroup?) : View {
val view = inflater.inflate(R.layout.list_time_zone_row, parent, false)
view.tag = ViewHolder(view)
return view
}
private class ViewHolder(view: View) {
val name = view.findViewById<TextView>(R.id.timeZone)
val clock = view.findViewById<TextClock>(R.id.clock)
}
}
ListViewの動作がなんか重いと思ったら、ここを見直してみよう。
MainActivityにListViewを実装する
最後に、アクティビティにListView実装しよう。
package com.apppppp.trylistview
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.ListView
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val list = findViewById<ListView>(R.id.clockList)
val adapter = TimeZoneAdapter(this)
list.adapter = adapter
list.setOnItemClickListener { _, _, position, _ ->
val timeZone = adapter.getItem(position)
println(timeZone)
}
}
}
これで完成。
createViewが呼び出される回数を確認
createViewが何回呼び出されるか確認するために、カウンターを追加して測定してみた。Viewがリサイクルされるならば、カウンターの値は一定値以上にはならないはずだ。
private var count = 0
private fun createView(parent: ViewGroup?) : View {
println(count++)
val view = inflater.inflate(R.layout.list_time_zone_row, parent, false)
view.tag = ViewHolder(view)
return view
}
リストをスクロールすると11回まで呼び出され、それ以上は呼び出されなかった。これで、Viewがリサイクルされていることが確認できた。
今回のプロジェクトファイルはGitHubへ上げておいたのでご参考に。 AndroidExercise/TryListView · GitHub
▼ こんな記事も書いてます。