RecyclerView を使用してスクロール可能なリストを表示する

スマートフォンでよく使用しているアプリについて考えてみましょう。ほとんどのアプリに少なくともリストが 1 つあります。通話履歴画面、連絡帳アプリ、ソーシャル メディア アプリには、どれにもデータのリストが表示されます。次のスクリーンショットのように、簡単な単語やフレーズのリストを表示するアプリもあれば、テキストや画像が入ったカードなどの複雑な項目を表示するアプリもあります。内容にかかわらず、データのリストを表示することは、Android の UI タスクとしては非常に一般的なものです。

cf10a913f9db0ee4.png

Android には、リストのあるアプリを作成するために RecyclerView が用意されています。RecyclerView は、非常に効率的に設計されていて、大きなリストを使用する場合でも、画面外にスクロールしたビューを再利用(リサイクル)します。画面外にスクロールされるリスト項目があると、RecyclerView でそのビューが次に表示されるリスト項目に再利用されます。具体的には、画面内にスクロールしてくる新たなコンテンツがその項目に設定されます。このような RecyclerView の動作により、処理時間が大幅に短縮され、リストをスムーズにスクロールできます。

次のシーケンスでは、1 つのビューにデータ ABC が設定されています。そのビューが画面外にスクロールされると、RecyclerView でそのビューが新しいデータ XYZ に再利用されます。

dcf4599789b9c2a1.png

この Codelab では、Affirmations アプリを作成します。Affirmations は、スクロール リストに 10 個のアファメーション(肯定的な宣言)をテキストで表示するシンプルなアプリです。これに続く Codelab では、さらに一歩進んで、アファメーションのそれぞれに想像をかき立てる画像を追加するとともに、アプリの UI を改良します。

前提条件

  • Android Studio でテンプレートからプロジェクトを作成できる。
  • アプリに文字列リソースを追加できる。
  • XML でレイアウトを定義できる。
  • Kotlin のクラスと継承(抽象クラスを含む)を理解している。
  • 既存のクラスを継承して、そのメソッドをオーバーライドできる。
  • developer.android.com にある、Android フレームワークで用意されているクラスについてのドキュメントを利用できる。

学習内容

  • RecyclerView を使用してデータのリストを表示する方法
  • コードをパッケージにまとめる方法
  • RecyclerView でアダプタを使用して、個々のリスト項目の外観をカスタマイズする方法

作成するアプリの概要

  • RecyclerView を使用してアファメーション文字列のリストを表示するアプリ

必要なもの

  • Android Studio バージョン 4.1 以上がインストールされているパソコン。

Empty Activity プロジェクトを作成する

新しいプロジェクトを作成する前に、Android Studio 4.1 以降を使用していることを確認してください。

  1. Android Studio で、[Empty Activity] テンプレートを使用して、新しい Kotlin プロジェクトを開始します。
  2. アプリの [Name] に「Affirmations」を、[Package name] に「com.example.affirmations」を入力し、[Minimum SDK] に [API Level 19] を選びます。
  3. [Finish] をクリックして、プロジェクトを作成します。

Affirmations アプリを作成するための次のステップは、リソースの追加です。以下をプロジェクトに追加します。

  • アプリにアファメーションとして表示する文字列リソース
  • アプリにアファメーションのリストを供給するデータのソース

アファメーション文字列を追加する

  1. [Project] ウィンドウで、[app] > [res] > [values] > [strings.xml] を開きます。このファイルには現在、アプリ名の文字列リソースが 1 つあるだけです。
  2. strings.xml に、以下のアファメーションを別々の文字列リソースとして追加します。それらに、affirmation1affirmation2 というように名前を付けます。

アファメーションのテキスト

I am strong.
I believe in myself.
Each day is a new opportunity to grow and be a better version of myself.
Every challenge in my life is an opportunity to learn from.
I have so much to be grateful for.
Good things are always coming into my life.
New opportunities await me at every turn.
I have the courage to follow my heart.
Things will unfold at precisely the right time.
I will be present in all the moments that this day brings.

完成した strings.xml ファイルは次のようになります。

<resources>
    <string name="app_name">Affirmations</string>
    <string name="affirmation1">I am strong.</string>
    <string name="affirmation2">I believe in myself.</string>
    <string name="affirmation3">Each day is a new opportunity to grow and be a better version of myself.</string>
    <string name="affirmation4">Every challenge in my life is an opportunity to learn from.</string>
    <string name="affirmation5">I have so much to be grateful for.</string>
    <string name="affirmation6">Good things are always coming into my life.</string>
    <string name="affirmation7">New opportunities await me at every turn.</string>
    <string name="affirmation8">I have the courage to follow my heart.</string>
    <string name="affirmation9">Things will unfold at precisely the right time.</string>
    <string name="affirmation10">I will be present in all the moments that this day brings.</string>
</resources>

文字列リソースを追加したので、コード内で R.string.affirmation1R.string.affirmation2 として参照できます。

新規パッケージを作成する

コードを論理的に整理すると、自分や他のデベロッパーにとってコードの理解、メンテナンス、拡張が容易になります。書類をフォルダに整理するのと同じように、コードをファイルやパッケージに整理できます。

パッケージとは

  1. Android Studio の [Project] ウィンドウ(Android)で、Affirmations アプリの [app] > [java] にある新しいプロジェクト ファイルを確認します。次のスクリーンショットのように、コード用のパッケージが 1 つ(com.example.affirmations)と、テストファイル用のパッケージが 2 つ(com.example.affirmations (androidTest)com.example.affirmations (test))の合計 3 つのパッケージが表示されているはずです。

809a0d77a0759dc5.png

  1. このように、パッケージ名は、いくつかの単語をピリオドで区切ったものです。

パッケージには 2 つの使い方があります。

  • コードの各部分に対して個別にパッケージを作成する。たとえば、データを扱うクラスと UI を構築するクラスを別々のパッケージに分割することがよくあります。
  • コード内で他のパッケージのコードを使用する。他のパッケージのクラスを使用するには、それをビルドシステムの依存関係で定義する必要があります。また、コード内で import するのも標準的な方法です。こうすると、完全修飾名(android.widget.TextView など)の代わりに短縮名(TextView など)を使用できます。たとえば、すでに sqrtimport kotlin.math.sqrt)や Viewimport android.view.View)などのクラスのために import ステートメントを使用しています。

Affirmations アプリでは、Android クラスと Kotlin クラスをインポートするだけでなく、アプリを整理して複数のパッケージに分割します。アプリにあるクラスが少ない場合でも、パッケージを使用して機能ごとにクラスをグループ化することをおすすめします。

パッケージの命名

パッケージ名は、グローバルに一意である限り、どのようなものでも構いません。他に同じ名前の公開パッケージがあってはなりません。膨大な数のパッケージがあり、ランダムな一意の名前を思い付くのは難しいので、プログラマーはパッケージ名の作成と理解が簡単になるように規則を使用しています。

  • 通常、パッケージ名は、小文字で表された名前の各部分を、一般的なものから限定的なものという順にして、ピリオドで区切って構成したものです。重要: ピリオドは名前の一部にすぎません。コード内の階層を示すものではなく、フォルダ構造を規定するものでもありません。
  • インターネット ドメインはグローバルに一意であるため、名前の最初の部分にはドメイン(通常はデベロッパーまたは組織のドメイン)を使用するのが一般的です。
  • パッケージ名を適切に選ぶことで、パッケージの内容とパッケージ同士の関係を示すことができます。
  • このコードのように、コード例では com.example の後にアプリ名が続くものがよく使用されます。

既定のパッケージ名とその内容の例を以下に示します。

  • kotlin.math - 数学関係の関数と定数
  • android.widget - ビュー(TextView など)

パッケージを作成する

  1. Android Studio の [Project] ペインで [app] > [java] > [com.example.affirmations] を右クリックし、[New] > [Package] を選択します。

39f35a81a574b7ee.png

  1. [New Package] ポップアップで、パッケージ名の接頭辞が提案されます。提案されるパッケージ名の最初の部分は、右クリックしたパッケージの名前です。パッケージ名によってパッケージの階層が作られることはありませんが、内容の関連性と編成を反映させるために名前の一部が再利用されます。
  2. ポップアップで、提案されたパッケージ名の末尾に「model」を追加します。デベロッパーは通常、データをモデル化(または表現)するクラスのパッケージ名として「model」を使用します。

3d392f8da53adc6f.png

  1. Enter キーを押します。これにより、com.example.affirmations(ルート)パッケージの下に新しいパッケージが作成されます。この新しいパッケージには、アプリで定義されたデータ関連のクラスがすべて含まれます。

Affirmation データクラスを作成する

このタスクでは、Affirmation. という名前のクラスを作成します。Affirmation のオブジェクト インスタンスは 1 つのアファメーションを表し、アファメーション文字列のリソース ID を含みます。

  1. com.example.affirmations.model パッケージを右クリックし、[New] > [Kotlin File/Class] を選択します。

d9b07905f456ab1.png

  1. ポップアップで [Class] を選択し、クラスの名前として「Affirmation」と入力します。これにより、Affirmation.kt という名前の新しいファイルが model パッケージに作成されます。
  2. クラス定義の前に data キーワードを追加して、Affirmation をデータクラスにします。データクラスには少なくとも 1 つのプロパティを定義する必要があるため、このままではエラーが残ります。

Affirmation.kt

package com.example.affirmations.model

data class Affirmation {
}

Affirmation のインスタンスを作成するときは、アファメーション文字列のリソース ID を渡す必要があります。リソース ID は整数です。

  1. val 整数パラメータ stringResourceIdAffirmation クラスのコンストラクタに追加します。これによりエラーを回避できます。
package com.example.affirmations.model

data class Affirmation(val stringResourceId: Int)

データソースとなるクラスを作成する

アプリに表示されるデータは、さまざまなソースから取得されます(アプリ プロジェクト内、データのダウンロードにインターネットへの接続が必要な外部ソースなど)。そのため、データの形式が必要な形式と完全には一致していない場合があります。アプリの他の部分で、データの取得元やデータの元々の形式を気にするべきではありません。このようなデータの準備は、アプリのためにデータを準備する別の Datasource クラスで行うことが可能であり、そうすべきです。

データの準備は別の関心事であるため、Datasource クラスを別の data パッケージに入れます。

  1. Android Studio の [Project] ウィンドウで [app] > [java] > [com.example.affirmations] を右クリックし、[New] > [Package] を選択します。
  2. パッケージ名の最後の部分として「data」と入力します。
  3. data パッケージを右クリックして、[new Kotlin File/Class] を選択します。
  4. クラス名として「Datasource」と入力します。
  5. Datasource クラス内に、loadAffirmations() という名前の関数を作成します。

loadAffirmations() 関数は Affirmations のリストを返す必要があります。そのために、リストを作成し、それに各リソース文字列の Affirmation インスタンスを入れます。

  1. メソッド loadAffirmations() の戻り値の型として List<Affirmation> を宣言します。
  2. loadAffirmations() の本体に、return ステートメントを追加します。
  3. return キーワードの後で、listOf<>() を呼び出して List を作成します。
  4. 山かっこ「<>」内で、リスト項目の型を Affirmation に指定します。必要に応じて、com.example.affirmations.model.Affirmation をインポートします。
  5. かっこ内で、以下に示すように、Affirmation を作成し、リソース ID として R.string.affirmation1 を渡します。
Affirmation(R.string.affirmation1)
  1. 残りの Affirmation オブジェクトをカンマで区切って全アファメーションのリストに追加します。完成したコードは次のようになります。

Datasource.kt

package com.example.affirmations.data

import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

class Datasource {

    fun loadAffirmations(): List<Affirmation> {
        return listOf<Affirmation>(
            Affirmation(R.string.affirmation1),
            Affirmation(R.string.affirmation2),
            Affirmation(R.string.affirmation3),
            Affirmation(R.string.affirmation4),
            Affirmation(R.string.affirmation5),
            Affirmation(R.string.affirmation6),
            Affirmation(R.string.affirmation7),
            Affirmation(R.string.affirmation8),
            Affirmation(R.string.affirmation9),
            Affirmation(R.string.affirmation10)
        )
    }
}

(任意)TextView 内に Affirmations リストのサイズを表示する

アファメーションのリストを作成できることを確認するために、loadAffirmations() を呼び出して、Empty Activity アプリ テンプレートの TextView に、返されたアファメーションのリストのサイズを表示します。

  1. layouts/activity_main.xml で、テンプレートの TextView に、textviewid を付与します。
  2. onCreate() メソッドの MainActivity の今あるコードの後で、textview への参照を取得します。
val textView: TextView = findViewById(R.id.textview)
  1. 次に、アファメーション リストを作成して、そのサイズを表示するコードを追加します。Datasource を作成して loadAffirmations() を呼び出し、返されたリストのサイズを取得して文字列に変換し、textViewtext に代入します。
textView.text = Datasource().loadAffirmations().size.toString()
  1. アプリを実行します。次のような画面が表示されるはずです。

b4005973d4a0efc8.png

  1. MainActivity に追加したコードを削除します。

このタスクでは、Affirmations のリストを表示するように RecyclerView をセットアップします。

RecyclerView の作成と使用は、多くの部分に分割されます。これは役割分担と考えられます。下の図にその概要を示します。各部分を実装しながら学習してゆきます。

  • item - 表示するリストの 1 つのデータ項目です。アプリ内の 1 つの Affirmation オブジェクトを表します。
  • Adapter - データを受け取って RecyclerView で表示できるようにします。
  • ViewHolders - アファメーションの表示に使用および再利用する RecyclerView のビューのプールです。
  • RecyclerView - 画面上のビューです。

4e9c18b463f00bf7.png

RecyclerView をレイアウトに追加する

Affirmations アプリは、MainActivity という名前の単一のアクティビティで構成され、そのレイアウト ファイルは activity_main.xml という名前です。まず、MainActivity のレイアウトに RecyclerView を追加する必要があります。

  1. activity_main.xml を開きます([app] > [res] > [layout] > [activity_main.xml])。
  2. まだ使用していない場合は、[Split view] に切り替えます。

7e7c3e8429267dac.png

  1. TextView を削除します。

現在のレイアウトは ConstraintLayout を使用しています。ConstraintLayout は、レイアウト内で複数の子ビューを配置する場合に最適で柔軟性があります。レイアウトには RecyclerView という子ビュー 1 つしかないため、単一の子ビューを保持するために使用する FrameLayout というシンプルな ViewGroup に変更できます。

9e6f235be5fa31a8.png

  1. XML で、ConstraintLayoutFrameLayout に置き換えます。完成したレイアウトは次のようになります。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
</FrameLayout>
  1. [Design] ビューに切り替えます。
  2. [Palette] で [Containers] を選択し、[RecyclerView] を見つけます。
  3. [RecyclerView] をレイアウトにドラッグします。
  4. [Add Project Dependency] ポップアップが表示された場合は、それを読んで [OK] をクリックします(表示されない場合は、何もする必要はありません)。
  5. Android Studio の処理が終わり、レイアウトに RecyclerView が表示されるまで待ちます。
  6. 必要に応じて、RecyclerViewlayout_width 属性と layout_height 属性を match_parent に変更して、RecyclerView が画面全体に表示されるようにします。
  7. RecyclerView のリソース ID を recycler_view に設定します。

RecyclerView では、線形リストやグリッドなど、さまざまな方法で項目を表示できます。項目の並べ替えには、LayoutManager を使用します。Android フレームワークでは、基本的な項目レイアウト用のレイアウト マネージャーが用意されています。Affirmations アプリでは項目を縦方向のリストとして表示するので、LinearLayoutManager を使用できます。

  1. [Code] ビューに戻ります。XML コード内の RecyclerView 要素に、次に示すように LinearLayoutManagerRecyclerView のレイアウト マネージャー属性として追加します。
app:layoutManager="LinearLayoutManager"

画面よりも長い項目のリストをスクロールするために、縦方向のスクロールバーを追加する必要があります。

  1. RecyclerView 内の android:scrollbars 属性を vertical に設定します。
android:scrollbars="vertical"

完成した XML レイアウトは次のようになります。

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layoutManager="LinearLayoutManager" />

</FrameLayout>
  1. アプリを実行します。

プロジェクトは問題なくコンパイルされ動作するはずです。ただし、重要なコードを入れていないため、アプリには白い背景のみが表示されます。この時点で、データのソースがあり、RecyclerView がレイアウトに追加されていますが、RecyclerView には Affirmation オブジェクトの表示方法に関する情報がありません。

RecyclerView のアダプタを実装する

アプリには、Datasource からデータを取得して、各 AffirmationRecyclerView 内の項目として表示できるように整形する手段が必要です。

アダプタは、データを RecyclerView で使用できる形に適応させるデザイン パターンです。この場合、loadAffirmations() によって返されたリストから Affirmation インスタンスを受け取って、それをリスト項目ビューに変換して、RecyclerView. に表示できるようにするアダプタが必要です。

アプリを実行すると、RecyclerView では、データを画面にどのように表示するか決めるために、アダプタが使用されます。RecyclerView からアダプタに対して、リストの最初のデータ項目用に新しいリスト項目ビューを作成するように指示が送られます。ビューが作成されると、アダプタは項目を描画するためのデータを要求されます。この動作は、全画面を埋めるのに十分な量のビューが RecyclerView に提供されるまで繰り返されます。一度に 3 つのリスト項目ビューだけで画面が満たされる場合、RecyclerView からアダプタに対して、10 項目すべてのリスト項目ビューではなく、3 つのリスト項目ビューを準備するように指示が送られます。

このステップでは、Affirmation オブジェクト インスタンスを RecyclerView オブジェクトに表示できるように適応させるアダプタを作成します。

アダプタを作成する

アダプタは複数の部分で構成されており、このコースでこれまでに作成したよりも、はるかに複雑で大量のコードを作成することになります。最初から詳細を十分に理解しなくても構いません。RecyclerView を使ってアプリ全体を完成させると、すべてのパーツがどのように組み合わされるのかを深く理解できます。RecyclerView を使って作成する今後のアプリのベースとして、このコードを再利用することもできます。

項目のレイアウトを作成する

RecyclerView の項目ごとに個別のレイアウトがあり、それを別々のレイアウト ファイルで定義します。文字列を表示するだけなので、項目レイアウトに TextView を使用できます。

  1. [res] > [layout] に、list_item.xml という名前の新しい空のファイルを作成します。
  2. [Code] ビューで list_item.xml を開きます。
  3. TextViewid item_title を追加します。
  4. 次のコードのように、layout_widthlayout_heightwrap_content を追加します。

このリスト項目レイアウトは後でインフレートされ、親 RecyclerView の子として追加されるため、レイアウトを囲む ViewGroup は必要ありません。

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

または、[File] > [New] > [Layout Resource File] を使用して、[File name] に list_item.xml を、[Root element] に TextView を設定します。次に、生成されたコードを上記のコードに合わせて修正します。

dbb34ca516c804c6.png

ItemAdapter クラスを作成する

  1. Android Studio の [Project] ペインで [app] > [java] > [com.example.affirmations] を右クリックし、[New] > [Package] を選択します。
  2. パッケージ名の最後の部分として「adapter」と入力します。
  3. adapter パッケージを右クリックして、[New] > [Kotlin File/Class] を選択します。
  4. クラス名として「ItemAdapter」と入力し、終了すると、ItemAdapter.kt ファイルが開きます。

ItemAdapter のコンストラクタにパラメータを追加して、アファメーションをアダプタに渡すようにする必要があります。

  1. List<Affirmation> 型の dataset という val のパラメータを ItemAdapter コンストラクタに追加します。必要に応じて、Affirmation をインポートします。
  2. dataset は、このクラスでのみ使用されるため、private にします。

ItemAdapter.kt

import com.example.affirmations.model.Affirmation

class ItemAdapter(private val dataset: List<Affirmation>) {

}

ItemAdapter では、文字列リソースを解決する方法に関する情報が必要です。この情報とアプリに関するその他の情報は、ItemAdapter インスタンスに渡すことができる Context オブジェクト インスタンスに格納されます。

  1. Context 型の context という val のパラメータを ItemAdapter コンストラクタに追加します。これをコンストラクタの最初のパラメータにします。
class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

}

ViewHolder を作成する

RecyclerView に項目ビューとの直接のやり取りはありませんが、代わりに ViewHolders を取り扱います。ViewHolderRecyclerView 内の 1 つのリスト項目ビューを表し、可能であれば再利用できます。ViewHolder インスタンスは、リスト項目レイアウト内の個々のビューへの参照を保持します(「ビューホルダー」という名前の由来です)。これにより、リスト項目ビューを新しいデータで更新するのが簡単になります。ビューホルダーにより、RecyclerView で画面での効率的なビューの移動に使用される情報も追加されます。

  1. ItemAdapter クラスの中の ItemAdapter の右波かっこの前で、ItemViewHolder クラスを作成します。
class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

    class ItemViewHolder()
}
  • クラスを別のクラスの中で定義することは、ネストされたクラスの作成と呼ばれます。
  • ItemViewHolderItemAdapter でのみ使用されるため、ItemAdapter 内に作成することで、この関係が示されます。これは必須ではありませんが、他のデベロッパーがプログラムの構造を理解するのに役立ちます。
  1. View 型の private val view をパラメータとして ItemViewHolder クラスのコンストラクタに追加します。
  2. ItemViewHolderRecyclerViewViewHolder のサブクラスにして、view パラメータをスーパークラス コンストラクタに渡します。
  3. ItemViewHolder 内で、TextView 型の val プロパティ textView を定義します。これに、list_item.xml で定義した ID item_title のビューを代入します。
class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }
}

アダプタ メソッドをオーバーライドする

  1. 抽象クラス RecyclerView.Adapter を拡張する ItemAdapter のコードを追加します。山かっこ内にビューホルダーの型として ItemAdapter.ItemViewHolder を指定します。
class ItemAdapter(
    private val context: Context,
    private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }
}

RecyclerView.Adapter の抽象メソッドを実装する必要があるため、エラーが表示されます。

  1. ItemAdapter にカーソルを置き、Command+I キー(Windows の場合は Control+I キー)を押します。実装する必要があるメソッドの一覧(getItemCount()onCreateViewHolder()onBindViewHolder())が表示されます。

7a8a383a8633094b.png

  1. Shift+クリックを使用して 3 つの関数をすべて選択し、[OK] をクリックします。

これによって、以下のように 3 つのメソッドに適切なパラメータを持ったスタブが作成されます。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
    TODO("Not yet implemented")
}

override fun getItemCount(): Int {
    TODO("Not yet implemented")
}

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
    TODO("Not yet implemented")
}

これでエラーが表示されなくなります。次は、これらのメソッドを実装して、このアプリとして正しい動作になっていることを確認します。

getItemCount() を実装する

getItemCount() メソッドからは、データセットのサイズを返す必要があります。アプリのデータは、ItemAdapter コンストラクタに渡す dataset プロパティに含まれていて、そのサイズは size で取得できます。

  1. getItemCount() を次のように置き換えます。
override fun getItemCount() = dataset.size

これは以下の記述を簡略化したものです。

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

onCreateViewHolder() を実装する

onCreateViewHolder() メソッドは、RecyclerView の新しいビューホルダーを作成するためにレイアウト マネージャーから呼び出されます(再利用できるビューホルダーがないとき)。ビューホルダーが 1 つのリスト項目ビューを表すことに留意してください。

onCreateViewHolder() メソッドは、2 つのパラメータを受け取り、新しい ViewHolder を返します。

  • parent パラメータ: 新しいリスト項目ビューが子としてアタッチされるビューグループです。親は RecyclerView です。
  • viewType パラメータ: 同じ RecyclerView に複数の項目ビュータイプがある場合に重要となります。RecyclerView 内に異なるリスト項目レイアウトが表示される場合、異なる項目ビュータイプがあります。リサイクルできるのは同じ項目ビュータイプのビューだけです。今回は、1 つのリスト項目レイアウトと 1 つの項目ビュータイプだけを指定しているため、このパラメータを気にする必要はありません。
  1. onCreateViewHolder() メソッドで、渡されたコンテキスト(parentcontext)から LayoutInflater のインスタンスを取得します。レイアウト インフレーターは、XML レイアウトをビュー オブジェクトの階層にインフレートする方法を認識しています。
val adapterLayout = LayoutInflater.from(parent.context)
  1. LayoutInflater オブジェクト インスタンスを作成したら、ピリオドと、その後ろに実際のリスト項目ビューをインフレートする別のメソッド呼び出しを追加します。XML レイアウトのリソース ID R.layout.list_itemparent ビューグループを渡します。3 番目のブール引数は attachToRoot です。この項目は RecyclerView によって自動的にビュー階層に追加されるため、この引数は false にする必要があります。
val adapterLayout = LayoutInflater.from(parent.context)
       .inflate(R.layout.list_item, parent, false)

adapterLayout はリスト項目ビューへの参照を保持するようになりました(ここから、後で

TextView のような子ビューを見つけることができます)。

  1. onCreateViewHolder() で、ルートビューが adapterLayout である新しい ItemViewHolder インスタンスを返します。
return ItemViewHolder(adapterLayout)

現時点での onCreateViewHolder() のコードは次のとおりです。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
    // create a new view
    val adapterLayout = LayoutInflater.from(parent.context)
        .inflate(R.layout.list_item, parent, false)

    return ItemViewHolder(adapterLayout)
}

onBindViewHolder() を実装する

オーバーライドする必要がある最後のメソッドは onBindViewHolder() です。このメソッドは、レイアウト マネージャーからリスト項目ビューの内容を置き換えるために呼び出されます。

onBindViewHolder() メソッドには 2 つのパラメータがあります。以前に onCreateViewHolder() メソッドで作成された ItemViewHolder と、リスト内の現在の項目 position を表す int です。このメソッドでは、データセットから位置に基づいて適切な Affirmation オブジェクトを見つけます。

  1. onBindViewHolder() 内で val item を作成し、dataset 内の指定された position にある項目を取得します。
val item = dataset[position]

最後に、この項目に正しいデータが反映されるように、ビューホルダーによって参照されているすべてのビューを更新する必要があります。今回は、ビューが 1 つしかありません(ItemViewHolder 内の TextView)。TextView のテキストを、この項目の Affirmation 文字列を表示するように設定します。

  1. Affirmation オブジェクト インスタンスで、item.stringResourceId を呼び出すことにより、対応する文字列リソース ID を見つけることができます。ただし、これは整数なので、実際の文字列値へのマッピングを見つける必要があります。

Android フレームワークでは、文字列リソース ID を指定して getString() を呼び出すと、その文字列に関連付けられた文字列値が返されます。getString()Resources クラスのメソッドであり、Resources クラスのインスタンスは context インスタンスを通じて取得できます。

つまり、context.resources.getString() を呼び出して、文字列リソース ID を渡すことができます。結果の文字列は、holder ItemViewHolder にある textViewtext に設定できます。つまり、この行のコードでは、アファメーション文字列を表示するビューホルダーを更新しています。

holder.textView.text = context.resources.getString(item.stringResourceId)

完成した onBindViewHolder() メソッドは次のようになります。

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
    val item = dataset[position]
    holder.textView.text =  context.resources.getString(item.stringResourceId)
}

最終的なアダプタのコードは以下のようになります。

ItemAdapter.kt

package com.example.affirmations.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

/**
 * Adapter for the [RecyclerView] in [MainActivity]. Displays [Affirmation] data object.
 */
class ItemAdapter(
    private val context: Context,
    private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder.
    // Each data item is just an Affirmation object.
    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }

    /**
     * Create new views (invoked by the layout manager)
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        // create a new view
        val adapterLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.list_item, parent, false)

        return ItemViewHolder(adapterLayout)
    }

    /**
     * Replace the contents of a view (invoked by the layout manager)
     */
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = dataset[position]
        holder.textView.text = context.resources.getString(item.stringResourceId)
    }

    /**
     * Return the size of your dataset (invoked by the layout manager)
     */
    override fun getItemCount() = dataset.size
}

ItemAdapter を実装したので、このアダプタを使用するように RecyclerView に指示する必要があります。

RecyclerView を使用するように MainActivity を変更する

最後に、Datasource クラスと ItemAdapter クラスを使用し、RecyclerView の項目を作成して表示する必要があります。この処理は MainActivity で行います。

  1. MainActivity.kt を開きます。
  2. MainActivity, で、onCreate() メソッドに移動します。これ以降のステップで説明する新しいコードを、setContentView(R.layout.activity_main). の呼び出しの後に挿入します。
  3. Datasource のインスタンスを作成し、その loadAffirmations() メソッドを呼び出します。返されたアファメーションのリストを myDataset という val に保存します。
val myDataset = Datasource().loadAffirmations()
  1. recyclerView という変数を作成し、findViewById() を使用して、レイアウト内の RecyclerView への参照を見つけます。
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
  1. 作成した ItemAdapter クラスを使用するように recyclerView に指示するために、新しい ItemAdapter インスタンスを作成します。ItemAdapter には、このアクティビティのコンテキスト(this)と myDataset のアファメーションという 2 つのパラメータが必要です。
  2. ItemAdapter オブジェクトを recyclerViewadapter プロパティに代入します。
recyclerView.adapter = ItemAdapter(this, myDataset)
  1. アクティビティ レイアウトでは RecyclerView のレイアウト サイズが固定されているため、RecyclerViewsetHasFixedSize パラメータを true に設定できます。この設定は、パフォーマンスの向上にのみ必要です。内容を変更しても RecyclerView のレイアウト サイズが変わらないことがわかっている場合は、この設定を使用します。
recyclerView.setHasFixedSize(true)
  1. 完了すると、MainActivity のコードは次のようになります。

MainActivity.kt

package com.example.affirmations

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.adapter.ItemAdapter
import com.example.affirmations.data.Datasource

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initialize data.
        val myDataset = Datasource().loadAffirmations()

        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        recyclerView.adapter = ItemAdapter(this, myDataset)

        // Use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        recyclerView.setHasFixedSize(true)
    }
}
  1. アプリを実行します。アファメーション文字列のリストが画面に表示されます。

427c10d4f29d769d.png

これで完成です。RecyclerView とカスタム アダプタを使用してデータのリストを表示するアプリを作成しました。作成したコードをよく読んで、各部分がどのように連携するのかを理解しましょう。

このアプリには、アファメーションを表示するのに必要なパーツがすべて揃っていますが、製品版とするには不十分です。UI に改良の余地がありそうです。次の Codelab では、コードを改良して、アプリに画像を追加する方法を学習し、UI を改良します。

この Codelab の解答コードは、以下に示すプロジェクトとモジュールにあります。ファイルの先頭の package ステートメントで示されるように、一部の Kotlin ファイルは別のパッケージに入っています。

res/values/strings.xml

<resources>
    <string name="app_name">Affirmations</string>
    <string name="affirmation1">I am strong.</string>
    <string name="affirmation2">I believe in myself.</string>
    <string name="affirmation3">Each day is a new opportunity to grow and be a better version of myself.</string>
    <string name="affirmation4">Every challenge in my life is an opportunity to learn from.</string>
    <string name="affirmation5">I have so much to be grateful for.</string>
    <string name="affirmation6">Good things are always coming into my life.</string>
    <string name="affirmation7">New opportunities await me at every turn.</string>
    <string name="affirmation8">I have the courage to follow my heart.</string>
    <string name="affirmation9">Things will unfold at precisely the right time.</string>
    <string name="affirmation10">I will be present in all the moments that this day brings.</string>
</resources>

affirmations/data/Datasource.kt

package com.example.affirmations.data

import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

class Datasource {

    fun loadAffirmations(): List<Affirmation> {
        return listOf<Affirmation>(
            Affirmation(R.string.affirmation1),
            Affirmation(R.string.affirmation2),
            Affirmation(R.string.affirmation3),
            Affirmation(R.string.affirmation4),
            Affirmation(R.string.affirmation5),
            Affirmation(R.string.affirmation6),
            Affirmation(R.string.affirmation7),
            Affirmation(R.string.affirmation8),
            Affirmation(R.string.affirmation9),
            Affirmation(R.string.affirmation10)
        )
    }
}

affirmations/model/Affirmation.kt

package com.example.affirmations.model

data class Affirmation(val stringResourceId: Int)

affirmations/MainActivty.kt

package com.example.affirmations

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.adapter.ItemAdapter
import com.example.affirmations.data.Datasource

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initialize data.
        val myDataset = Datasource().loadAffirmations()

        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        recyclerView.adapter = ItemAdapter(this, myDataset)

        // Use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        recyclerView.setHasFixedSize(true)
    }
}

affirmations/adapter/ItemAdapter.kt

package com.example.affirmations.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

/**
 * Adapter for the [RecyclerView] in [MainActivity]. Displays [Affirmation] data object.
 */
class ItemAdapter(
    private val context: Context,
    private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder.
    // Each data item is just an Affirmation object.
    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }

    /**
     * Create new views (invoked by the layout manager)
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        // create a new view
        val adapterLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.list_item, parent, false)

        return ItemViewHolder(adapterLayout)
    }

    /**
     * Replace the contents of a view (invoked by the layout manager)
     */
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = dataset[position]
        holder.textView.text = context.resources.getString(item.stringResourceId)
    }

    /**
     * Return the size of your dataset (invoked by the layout manager)
     */
    override fun getItemCount() = dataset.size
}

src/main/res/layout/activty_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layoutManager="LinearLayoutManager" />

</FrameLayout>

src/main/res/layout/list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
  • RecyclerView ウィジェットを使用すると、データのリストを表示できます。
  • RecyclerView は、アダプタ パターンを使用して、データの調整と表示を行います。
  • ViewHolder は、RecyclerView のためにビューを作成して保持します。
  • RecyclerView は、組み込みの LayoutManagers に付属しています。RecyclerView は、項目の配置を LayoutManagers に委譲します。

アダプタの実装は次のように行います。

  • アダプタ用に新しいクラスを作成します(例: ItemAdapter)。
  • 単一のリスト項目ビューを表すカスタム ViewHolder クラスを作成します。RecyclerView.ViewHolder クラスを拡張します。
  • ItemAdapter クラスを、RecyclerView.Adapter クラス(カスタムの ViewHolder クラス付き)を拡張するように書き換えます。
  • メソッド getItemsCount()onCreateViewHolder()onBindViewHolder() をアダプタ内に実装します。