【Kotlin】ListViewを簡単なモデルで理解しよう【Androidアプリ開発】

【Kotlin】ListViewを簡単なモデルで理解しよう【Androidアプリ開発】
【Kotlin】ListViewを簡単なモデルで理解しよう【Androidアプリ開発】

ここではAndroidのListViewを、簡単なモデルを使って実装してみる。本記事の完成イメージがこちら。

ゴール
ゴール

標準に搭載されているタイムゾーンの配列をTimeZone.getAvailableIDs()で取得して、一覧表示する単純な仕様となっている。APIなど複雑な処理はないのでリストビューの理解だけに集中できるように進めていく。

ListViewのレイアウトファイルを作る

配置したListViewの行のレイアウトファイルを作りたい場合は、ListViewをクリックして、listitemの項目の...をクリック。開いたダイアログの右上から、New layout Fileを選択して作成すると簡単だ。

こんな感じにレイアウトする。

リストの行のタップ領域高さは、48dp以上が推奨されているようだ。あまりに狭いと、ユーザビリティが悪くなるので気をつけよう。

基本のListView

BaseAdapterクラスを継承した、TimeZoneAdapterクラスを作成する。

kotlin
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で速度改善

実はさきほどのプログラムは、次の部分に問題がある。

kotlin
val timeZoneLabel = view.findViewById<TextView>(R.id.timeZone)

スクロールするたびに、findViewByIdを呼び出すのはコストがかかるのだ。つまりスクロール動作が遅くなる。そこで登場するのがViewHolderパターンだ。

Viewにはtagプロパティがあり、そこに任意のオブジェクトを1つ持つことができる。これを利用して、あらかじめtimeZoneLabelなどのインスタンスを生成して持たせておくのだ。convertViewはリサイクルされるので、tagに持たせたオブジェクトも再利用されるはずである。そして次のコードが、 ViewHolderパターンに書き換えたものだ。

kotlin
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実装しよう。

kotlin
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がリサイクルされるならば、カウンターの値は一定値以上にはならないはずだ。

kotlin
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

▼ こんな記事も書いてます。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

関連記事