ListViewを簡単なモデルで理解しよう【Android/Kotlin】

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へ上げておいたのでご参考に。

Anrdoidアプリ開発にオススメの書籍

こちらの本は、図や絵が多くてわかりやすく、Androidアプリ開発初心者でも理解しやすい内容。基本的なことはしっかりおさえられるので、この手の本は一冊読んでおくと良と思う。

基本からしっかり身につくAndroidアプリ開発入門
基本からしっかり身につくAndroidアプリ開発入門

圧倒的な多数のユーザーが使っているヤフーのアプリ。その制作の最前線にいる黒帯エンジニアが、ユーザーが使いやすいアプリの大切な基本をしっかりと解説します。

KindleAmazon

Kotlinの実践的な使い方が学べる内容の本で、プログラミング技術をもっと磨きたい人向け。センスの良いプログラミングがたくさん紹介されていて、個人的には「目からうろこ」の連続だった本。

Kotlinプログラミング
Kotlinプログラミング

Kotlinは、Javaとの相互運用を可能にし、Android OSでGoogleがフルサポートする静的型プログラミング言語です。この言語は、Javaだけでは十全ではない(Javaだけでは実装に手間がかかりすぎる)、軽量かつ豊かな表現形式や、他言語ではすでに実装されている最新の機能を盛り込んでいます。

KindleAmazon

最後まで読んでいただきありがとうございました。

「この記事が参考になったよ」という方は、ぜひ記事をシェアをしていただけるととても嬉しいです。

今後も有益な記事を書くモチベーションにつながりますので、どうかよろしくお願いいたします。↓↓↓↓↓↓↓

あなたにおすすめ