Android Kotlin の基礎 07.1: RecyclerView の基礎

この Codelab は、Android Kotlin の基礎コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。コースのすべての Codelab は、Android Kotlin の基礎の Codelab のランディング ページに一覧表示されています。

はじめに

この Codelab では、RecyclerView を使用してアイテムのリストを表示する方法について説明します。前の Codelab シリーズの睡眠トラッカー アプリを基に、推奨アーキテクチャで RecyclerView を使用してデータを表示する、より優れた汎用性の高い方法を学びます。

前提となる知識

以下について把握しておく必要があります。

  • アクティビティ、フラグメント、ビューを使用して基本的なユーザー インターフェース(UI)を構築する。
  • フラグメント間の移動、safeArgs を使用してフラグメント間でデータを渡す。
  • ビューモデル、ビューモデル ファクトリ、変換、LiveData とそのオブザーバーを使用します。
  • Room データベースの作成、DAO の作成、エンティティの定義。
  • データベース タスクやその他の長時間実行タスクにコルーチンを使用する。

学習内容

  • RecyclerViewAdapter および ViewHolder とともに使用して、アイテムのリストを表示する方法。

演習内容

  • 前のレッスンで作成した TrackMySleepQuality アプリを変更して、RecyclerView を使用して睡眠の質のデータを表示します。

この Codelab では、睡眠の質をトラッキングするアプリの RecyclerView 部分を構築します。このアプリは、Room データベースを使用して睡眠データを経時的に保存します。

スターターの睡眠トラッカー アプリには、下の図に示すように、フラグメントで表される 2 つの画面があります。

左側に表示されている最初の画面には、トラッキングの開始と停止を行うボタンがあります。この画面には、ユーザーのすべての睡眠データも表示されます。[消去] ボタンをクリックすると、アプリがユーザーのために収集したすべてのデータが完全に削除されます。右側の 2 つ目の画面は、睡眠の質の評価を選択するための画面です。

このアプリは、UI コントローラ、ViewModelLiveData を使用する簡略化されたアーキテクチャを使用します。また、このアプリは Room データベースを使用して睡眠データを永続化します。

最初の画面に表示される睡眠のリストは機能しますが、見栄えはよくありません。アプリは複雑なフォーマッタを使用して、テキスト ビューのテキスト文字列と品質の数値を生成します。また、この設計はスケーリングできません。この Codelab でこれらの問題をすべて修正すると、最終的なアプリは同じ機能を持ち、メイン画面は次のようになります。

データのリストやグリッドを表示することは、Android の UI タスクとしては非常に一般的なものです。リストは、単純なものから非常に複雑なものまでさまざまです。テキスト ビューのリストには、買い物リストなどのシンプルなデータが表示されることがあります。休暇の目的地を注釈付きでリスト表示するような複雑なリストでは、ヘッダー付きのスクロール可能なグリッド内に多くの詳細情報が表示されることがあります。

Android では、こうしたユースケースをすべてサポートするために RecyclerView ウィジェットが用意されています。

RecyclerView の最大のメリットは、大きなリストに対して非常に効率的であることです。

  • デフォルトでは、RecyclerView は、現時点で画面に表示されるアイテムの処理や描画のための作業のみを行います。たとえば、リストには 1,000 個のアイテムがあるものの、画面には 10 個しか表示されない場合は、RecyclerView はその 10 個の項目を描画するのに十分な作業しか行いません。ユーザーがスクロールすると、RecyclerView は画面に新たに表示されるアイテムを認識して、その描画に十分な作業のみを行います。
  • アイテムが画面外にスクロールされると、アイテムのビューがリサイクルされます。具体的には、画面内にスクロールしてくる新たなコンテンツがその項目に設定されます。このような RecyclerView の動作により、処理時間が大幅に短縮され、リストをスムーズにスクロールできます。
  • アイテムが変更された場合、RecyclerView はリスト全体を再描画するのではなく、そのアイテム 1 つを更新できます。複雑なアイテムのリストを表示する際に、効率が大幅に向上します。

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

アダプター パターン

コンセントの形状が異なる国間を移動したことがある方は、アダプターを使用してデバイスをコンセントに接続する方法をご存じでしょう。アダプターを使用すると、プラグのタイプを変換できます。これは、インターフェースを別のインターフェースに変換することに相当します。

ソフトウェア エンジニアリングのアダプター パターンは、オブジェクトが別の API と連携するのに役立ちます。RecyclerView は、アプリがデータを保存して処理する方法を変更することなく、アダプタを使用してアプリデータを RecyclerView が表示できるものに変換します。睡眠トラッカー アプリでは、ViewModel を変更せずに、Room データベースのデータを RecyclerView が表示できる形式に変換するアダプタを構築します。

RecyclerView の実装

RecyclerView にデータを表示するには、次の部分が必要です。

  • 表示するデータ。
  • レイアウト ファイルで定義された RecyclerView インスタンス。ビューのコンテナとして機能します。
  • 1 つのデータ項目のレイアウト。
    リスト項目がすべて同じように見える場合は、すべてに同じレイアウトを使用できますが、必須ではありません。アイテム レイアウトは、一度に 1 つのアイテム ビューを作成してデータで埋めることができるように、フラグメントのレイアウトとは別に作成する必要があります。
  • レイアウト マネージャー。
    レイアウト マネージャーは、ビュー内の UI コンポーネントの構成(レイアウト)を処理します。
  • ビューホルダー。
    ビューホルダーは ViewHolder クラスを拡張します。アイテムのレイアウトから 1 つのアイテムを表示するためのビュー情報が含まれています。ビューホルダーにより、RecyclerView で画面での効率的なビューの移動に使用される情報も追加されます。
  • アダプター。
    アダプターは、データを RecyclerView に接続します。ViewHolder に表示できるようにデータを適応させます。RecyclerView は、アダプタを使用して、データを画面にどのように表示するかを決定します。

このタスクでは、レイアウト ファイルに RecyclerView を追加し、RecyclerView に睡眠データを公開する Adapter を設定します。

ステップ 1: LayoutManager を使用して RecyclerView を追加する

このステップでは、fragment_sleep_tracker.xml ファイルの ScrollViewRecyclerView に置き換えます。

  1. GitHub から RecyclerViewFundamentals-Starter アプリをダウンロードします。
  2. アプリをビルドして実行します。データがシンプルなテキストとして表示されることを確認します。
  3. Android Studio の [Design] タブで fragment_sleep_tracker.xml レイアウト ファイルを開きます。
  4. [Component Tree] ペインで、ScrollView を削除します。この操作を行うと、ScrollView 内の TextView も削除されます。
  5. [Palette] パネルで、左側のコンポーネント タイプのリストをスクロールして [Containers] を見つけ、選択します。
  6. [Palette] ペインから [Component Tree] ペインに RecyclerView をドラッグします。RecyclerViewConstraintLayout の内側に配置します。

  1. 依存関係を追加するかどうかを尋ねるダイアログが開いたら、[OK] をクリックして、Android Studio が recyclerview 依存関係を Gradle ファイルに追加できるようにします。数秒後にアプリが同期されます。

  1. モジュールの build.gradle ファイルを開き、最後までスクロールして、次のコードのような新しい依存関係をメモします。
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. fragment_sleep_tracker.xml に戻します。
  2. [テキスト] タブで、次の RecyclerView コードを探します。
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. RecyclerViewsleep_listid を指定します。
android:id="@+id/sleep_list"
  1. RecyclerViewConstraintLayout 内の画面の残りの部分を占めるように配置します。そのため、RecyclerView の上端を [Start] ボタンに、下端を [Clear] ボタンに、両端を親に制約します。レイアウト エディタまたは XML で、次のコードを使用してレイアウトの幅と高さを 0 dp に設定します。
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/clear_button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/stop_button"
  1. RecyclerView XML にレイアウト マネージャーを追加します。すべての RecyclerView には、リスト内のアイテムの配置方法を指示するレイアウト マネージャーが必要です。Android には LinearLayoutManager が用意されています。これはデフォルトで、アイテムを全幅の行の縦方向のリストにレイアウトします。
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. [Design] タブに切り替えると、追加した制約によって RecyclerView が利用可能なスペースを埋めるように拡大されていることがわかります。

ステップ 2: リストアイテムのレイアウトとテキスト ビュー ホルダーを作成する

RecyclerView はコンテナにすぎません。このステップでは、RecyclerView 内に表示されるアイテムのレイアウトとインフラストラクチャを作成します。

できるだけ早く動作する RecyclerView を作成するため、最初は睡眠の質を数値で表示するだけの単純なリスト項目を使用します。これには、ビューホルダー TextItemViewHolder が必要です。データ用のビュー(TextView)も必要です。(後のステップでは、ビューホルダーとすべての睡眠データをレイアウトする方法について詳しく説明します)。

  1. text_item_view.xml というレイアウト ファイルを作成します。テンプレート コードは置き換えるため、ルート要素として何を使用しても構いません。
  2. text_item_view.xml で、指定されたコードをすべて削除します。
  3. TextView を追加し、左右に 16dp のパディング、テキストサイズを 24sp に設定します。幅は親に合わせ、高さはコンテンツをラップします。このビューは RecyclerView 内に表示されるため、ビューを ViewGroup 内に配置する必要はありません。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:textSize="24sp"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:layout_width="match_parent"       
    android:layout_height="wrap_content" />
  1. Util.kt を開きます。末尾までスクロールして、次の定義を追加します。これにより、TextItemViewHolder クラスが作成されます。コードは、ファイルの末尾の最後の閉じ中かっこの後に配置します。このビューホルダーは一時的なもので、後で置き換えるため、コードは Util.kt に記述します。
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. プロンプトが表示されたら、android.widget.TextViewandroidx.recyclerview.widget.RecyclerView をインポートします。

ステップ 3: SleepNightAdapter を作成する

RecyclerView を実装する際のコアタスクは、アダプターの作成です。アイテムビュー用のシンプルなビューホルダーと、各アイテムのレイアウトがあります。これで、アダプターを作成できます。アダプターはビューホルダーを作成し、RecyclerView で表示するデータを入力します。

  1. sleeptracker パッケージで、SleepNightAdapter という名前の新しい Kotlin クラスを作成します。
  2. SleepNightAdapter クラスが RecyclerView.Adapter を拡張するようにします。このクラスは、SleepNight オブジェクトを RecyclerView が使用できるものに適合させるため、SleepNightAdapter と呼ばれます。アダプタは使用するビューホルダーを認識する必要があるため、TextItemViewHolder を渡します。プロンプトが表示されたら必要なコンポーネントをインポートします。実装する必要がある必須メソッドがあるため、エラーが表示されます。
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. SleepNightAdapter の最上位で、データを保持する listOf SleepNight 変数を作成します。
var data =  listOf<SleepNight>()
  1. SleepNightAdapter で、getItemCount() をオーバーライドして、data の睡眠日数のリストのサイズを返します。RecyclerView は、アダプターが表示するアイテムの数を把握する必要があります。そのため、getItemCount() を呼び出します。
override fun getItemCount() = data.size
  1. SleepNightAdapter で、次のように onBindViewHolder() 関数をオーバーライドします。

    onBindViewHolder() 関数は、指定された位置にある 1 つのリストアイテムのデータを表示するために RecyclerView によって呼び出されます。onBindViewHolder() メソッドは、ビューホルダーとバインドするデータの位置という 2 つの引数を取ります。このアプリでは、ホルダーは TextItemViewHolder で、位置はリスト内の位置です。
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
  1. onBindViewHolder() 内で、データ内の指定された位置にある 1 つのアイテムの変数を作成します。
 val item = data[position]
  1. 作成した ViewHolder には、textView というプロパティがあります。onBindViewHolder() 内で、textViewtext を睡眠の質の数値に設定します。このコードは数字のリストのみを表示しますが、この簡単な例で、アダプターがデータをビューホルダーに取得して画面に表示する方法を確認できます。
holder.textView.text = item.sleepQuality.toString()
  1. SleepNightAdapter で、onCreateViewHolder() をオーバーライドして実装します。これは、RecyclerView がアイテムを表すビューホルダーを必要とするときに呼び出されます。

    この関数は 2 つのパラメータを受け取り、ViewHolder を返します。ビューホルダーを保持するビューグループである parent パラメータは、常に RecyclerView です。viewType パラメータは、同じ RecyclerView に複数のビューがある場合に使用されます。たとえば、テキスト ビュー、画像、動画のリストをすべて同じ RecyclerView に配置した場合、onCreateViewHolder() 関数はどのタイプのビューを使用するかを認識する必要があります。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
  1. onCreateViewHolder() で、LayoutInflater のインスタンスを作成します。

    レイアウト インフレーターは、XML レイアウトからビューを作成する方法を認識しています。context には、ビューを正しく拡張する方法に関する情報が含まれます。リサイクラー ビューのアダプターでは、常に parent ビューグループのコンテキスト(RecyclerView)を渡します。
val layoutInflater = LayoutInflater.from(parent.context)
  1. onCreateViewHolder() で、layoutinflater にインフレートをリクエストして view を作成します。

    ビューの XML レイアウトと、ビューの parent ビューグループを渡します。3 番目のブール引数は attachToRoot です。この項目は RecyclerView によって自動的にビュー階層に追加されるため、この引数は false にする必要があります。
val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. onCreateViewHolder() で、view で作成された TextItemViewHolder を返します。
return TextItemViewHolder(view)
  1. アダプターは data が変更されたときに RecyclerView に通知する必要があります。これは、RecyclerView がデータについて何も知らないためです。アダプターから渡されたビューホルダーのみを認識します。

    表示しているデータが変更されたときに RecyclerView に通知するには、SleepNightAdapter クラスの先頭にある data 変数にカスタム セッターを追加します。セッターで data に新しい値を指定し、notifyDataSetChanged() を呼び出して新しいデータでリストの再描画をトリガーします。
var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

ステップ 4: RecyclerView に Adapter を通知する

RecyclerView は、ビューホルダーの取得に使用するアダプターについて知る必要があります。

  1. SleepTrackerFragment.kt を開きます。
  2. onCreateview() でアダプターを作成します。このコードは、ViewModel モデルの作成後、return ステートメントの前に配置します。
val adapter = SleepNightAdapter()
  1. adapterRecyclerView と関連付けます。
binding.sleepList.adapter = adapter
  1. プロジェクトをクリーンアップして再ビルドし、binding オブジェクトを更新します。

    binding.sleepList または binding.FragmentSleepTrackerBinding に関するエラーが引き続き表示される場合は、キャッシュを無効にして再起動します。([File] > [Invalidate Caches / Restart] を選択します)。

    ここでアプリを実行すると、エラーは発生しませんが、[Start]、[Stop] の順にタップしてもデータは表示されません。

ステップ 5: アダプタにデータを取得する

ここまでの作業により、アダプターと、アダプターから RecyclerView にデータを取得する方法ができました。次に、ViewModel からアダプターにデータを取得する必要があります。

  1. SleepTrackerViewModel を開きます。
  2. すべての睡眠夜を保存する nights 変数を見つけます。これが表示するデータです。nights 変数は、データベースで getAllNights() を呼び出すことで設定されます。
  3. この変数にアクセスする必要があるオブザーバーを作成するため、nights から private を削除します。宣言は次のようになります。
val nights = database.getAllNights()
  1. database パッケージで、SleepDatabaseDao を開きます。
  2. getAllNights() 関数を見つけます。この関数は、SleepNight 値のリストを LiveData として返します。つまり、nights 変数には Room によって更新される LiveData が含まれており、nights を監視して変更を把握できます。
  3. SleepTrackerFragment を開きます。
  4. onCreateView() で、adapter の作成の下に、nights 変数のオブザーバーを作成します。

    フラグメントの viewLifecycleOwner をライフサイクル オーナーとして指定することで、このオブザーバーがアクティブになるのは RecyclerView が画面に表示されているときのみになります。
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. オブザーバー内で、null 以外の値(nights の場合)を取得するたびに、その値をアダプターの data に割り当てます。オブザーバーとデータの設定の完成したコードは次のとおりです。
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. コードをビルドして実行します。

    アダプタが動作している場合は、睡眠の質の数値が一覧表示されます。左側のスクリーンショットは、[開始] をタップした後の -1 を示しています。右側のスクリーンショットは、[停止] をタップして睡眠の質を選択した後に更新された睡眠の質の数値を示しています。

ステップ 6: ビューホルダーが再利用される仕組みを確認する

RecyclerView はビューホルダーをリサイクルします。つまり、ビューホルダーを再利用します。ビューが画面外にスクロールされると、RecyclerView はそのビューを画面上にスクロールされるビューに再利用します。

これらのビューホルダーは再利用されるため、onBindViewHolder() は、前のアイテムがビューホルダーに設定したカスタマイズをすべて設定またはリセットするようにしてください。

たとえば、睡眠の質が 1 以下で、睡眠の質が低いことを表すビューホルダーのテキストの色を赤に設定できます。

  1. SleepNightAdapter クラスで、onBindViewHolder() の末尾に次のコードを追加します。
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. アプリを実行します。
  2. 睡眠の質の低いデータを追加すると、数値が赤色になります。
  3. 画面に赤い大きな数字が表示されるまで、睡眠の質の高い評価を追加します。

    RecyclerView はビューホルダーを再利用するため、最終的に赤いビューホルダーの 1 つを高品質の評価に再利用します。高い評価が誤って赤色で表示される。

  1. この問題を解決するには、品質が 1 以下でない場合に色を黒に設定する else ステートメントを追加します。

    両方の条件を明示的に指定することで、ビューホルダーは各アイテムに正しいテキストの色を使用します。
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. アプリを実行すると、数字は常に正しい色で表示されます。

これで、これで、完全に機能する基本的な RecyclerView が完成しました。

このタスクでは、シンプルなビューホルダーを、睡眠に関するより多くのデータを表示できるビューホルダーに置き換えます。

Util.kt に追加した単純な ViewHolder は、TextViewTextItemViewHolder でラップするだけです。

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

では、RecyclerViewTextView を直接使用しないのはなぜでしょうか?この 1 行のコードで多くの機能が提供されます。ViewHolder は、アイテムビューと、RecyclerView 内のアイテムビューの位置に関するメタデータを記述します。RecyclerView は、この機能を利用して、リストのスクロール時にビューを正しく配置したり、Adapter でアイテムが追加または削除されたときにビューをアニメーション化するなどの面白い処理を行ったりします。

RecyclerViewViewHolder に保存されているビューにアクセスする必要がある場合は、ビューホルダーの itemView プロパティを使用してアクセスできます。RecyclerView は、画面に表示するアイテムをバインドするとき、境界線などのビューの周りに装飾を描画するとき、ユーザー補助機能を実装するときに itemView を使用します。

ステップ 1: アイテム レイアウトを作成する

このステップでは、1 つのアイテムのレイアウト ファイルを作成します。レイアウトは、睡眠の質を表す ImageView、睡眠時間を表す TextView、質をテキストで表す TextView を含む ConstraintLayout で構成されています。レイアウトはすでに作成済みなので、提供された XML コードをコピーして貼り付けます。

  1. 新しいレイアウト リソース ファイルを作成し、list_item_sleep_night という名前を付けます。
  2. ファイル内のコードをすべて次のコードに置き換えます。作成したレイアウトを確認します。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content">

   <ImageView
       android:id="@+id/quality_image"
       android:layout_width="@dimen/icon_size"
       android:layout_height="60dp"
       android:layout_marginStart="16dp"
       android:layout_marginTop="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:srcCompat="@drawable/ic_sleep_5" />

   <TextView
       android:id="@+id/sleep_length"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginStart="8dp"
       android:layout_marginTop="8dp"
       android:layout_marginEnd="16dp"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/quality_image"
       app:layout_constraintTop_toTopOf="@+id/quality_image"
       tools:text="Wednesday" />

   <TextView
       android:id="@+id/quality_string"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginTop="8dp"
       app:layout_constraintEnd_toEndOf="@+id/sleep_length"
       app:layout_constraintHorizontal_bias="0.0"
       app:layout_constraintStart_toStartOf="@+id/sleep_length"
       app:layout_constraintTop_toBottomOf="@+id/sleep_length"
       tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Android Studio の [Design] タブに切り替えます。デザイン ビューでは、レイアウトは下の左側のスクリーンショットのようになります。ブループリント ビューでは、右側のスクリーンショットのようになります。

ステップ 2: ViewHolder を作成する

  1. SleepNightAdapter.kt を開きます。
  2. SleepNightAdapter 内に ViewHolder というクラスを作成し、RecyclerView.ViewHolder を拡張するようにします。
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. ViewHolder 内で、ビューへの参照を取得します。この ViewHolder が更新するビューへの参照が必要です。この ViewHolder をバインドするたびに、画像と両方のテキスト ビューにアクセスする必要があります。(このコードは後でデータ バインディングを使用するように変換します)。
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)

ステップ 3: SleepNightAdapter で ViewHolder を使用する

  1. SleepNightAdapter 定義で、TextItemViewHolder の代わりに、先ほど作成した SleepNightAdapter.ViewHolder を使用します。
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

onCreateViewHolder() を更新します。

  1. ViewHolder を返すように onCreateViewHolder() のシグネチャを変更します。
  2. レイアウト インフレータを変更して、正しいレイアウト リソース list_item_sleep_nightを使用します。
  3. TextView へのキャストを削除します。
  4. TextItemViewHolder を返すのではなく、ViewHolder を返します。

    更新された onCreateViewHolder() 関数の完成形は次のとおりです。
    override fun onCreateViewHolder(
            parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = 
            LayoutInflater.from(parent.context)
        val view = layoutInflater
                .inflate(R.layout.list_item_sleep_night, 
                         parent, false)
        return ViewHolder(view)
    }

onBindViewHolder() を更新します。

  1. onBindViewHolder() のシグネチャを変更して、holder パラメータが TextItemViewHolder ではなく ViewHolder になるようにします。
  2. onBindViewHolder() 内で、item の定義以外のすべてのコードを削除します。
  3. このビューの resources への参照を保持する val res を定義します。
val res = holder.itemView.context.resources
  1. sleepLength テキスト ビューのテキストを再生時間に設定します。スターター コードで提供されている書式設定関数を呼び出す次のコードをコピーします。
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
  1. convertDurationToFormatted() を定義する必要があるため、エラーが発生します。Util.kt を開き、コードとその関連インポートのコメントを解除します。([Code] > [Comment with Line comments] を選択します)。
  2. onBindViewHolder() に戻り、convertNumericQualityToString() を使用して画質を設定します。
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. これらの関数を手動でインポートする必要がある場合があります。
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. 品質の正しいアイコンを設定します。新しい ic_sleep_active アイコンは、スターター コードで提供されています。
holder.qualityImage.setImageResource(when (item.sleepQuality) {
   0 -> R.drawable.ic_sleep_0
   1 -> R.drawable.ic_sleep_1
   2 -> R.drawable.ic_sleep_2
   3 -> R.drawable.ic_sleep_3
   4 -> R.drawable.ic_sleep_4
   5 -> R.drawable.ic_sleep_5
   else -> R.drawable.ic_sleep_active
})
  1. 完成した更新後の onBindViewHolder() 関数を次に示します。ViewHolder のすべてのデータを設定しています。
   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = data[position]
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. アプリを実行します。下のスクリーンショットのように、睡眠の質を示すアイコンと、睡眠時間と睡眠の質を示すテキストが表示されます。

RecyclerView が完了しました。AdapterViewHolder を実装する方法を学び、それらを組み合わせて RecyclerView Adapter でリストを表示しました。

ここまでのコードは、アダプターとビューホルダーを作成するプロセスを示しています。ただし、このコードは改善できます。表示するコードとビューホルダーを管理するコードが混在しており、onBindViewHolder()ViewHolder を更新する方法の詳細を知っています。

本番環境のアプリでは、複数のビューホルダー、より複雑なアダプター、複数のデベロッパーによる変更が行われる可能性があります。ビューホルダーに関連するものはすべてビューホルダーに含めるように、コードを構造化する必要があります。

ステップ 1: onBindViewHolder() をリファクタリングする

このステップでは、コードをリファクタリングし、すべてのビューホルダー機能を ViewHolder に移動します。このリファクタリングの目的は、アプリのユーザー インターフェースを変更することではなく、デベロッパーがコードをより簡単かつ安全に操作できるようにすることです。幸いなことに、Android Studio にはこうした作業に役立つツールがあります。

  1. SleepNightAdapteronBindViewHolder() で、変数 item を宣言するステートメント以外のすべてを選択します。
  2. 右クリックして、[Refactor] > [Extract] > [Function] を選択します。
  3. 関数に bind という名前を付け、推奨されるパラメータを受け入れます。[OK] をクリックします。

    bind() 関数は onBindViewHolder() の下に配置されます。
    private fun bind(holder: ViewHolder, item: SleepNight) {
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. bind()holder パラメータの holder という単語にカーソルを合わせます。Alt+Enter(Mac の場合は Option+Enter)キーを押して、インテンション メニューを開きます。[Convert parameter to receiver] を選択して、次のシグネチャを持つ拡張関数に変換します。
private fun ViewHolder.bind(item: SleepNight) {...}
  1. bind() 関数を切り取って ViewHolder に貼り付けます。
  2. bind() を公開しました。
  3. 必要に応じて、bind() をアダプターにインポートします。
  4. ViewHolder になったため、署名の ViewHolder 部分を削除できます。ViewHolder クラスの bind() 関数の最終的なコードは次のとおりです。
fun bind(item: SleepNight) {
   val res = itemView.context.resources
   sleepLength.text = convertDurationToFormatted(
           item.startTimeMilli, item.endTimeMilli, res)
   quality.text = convertNumericQualityToString(
           item.sleepQuality, res)
   qualityImage.setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

ステップ 2: onCreateViewHolder をリファクタリングする

アダプタの onCreateViewHolder() メソッドは、現在 ViewHolder のレイアウト リソースからビューをインフレートしています。ただし、インフレーションはアダプターとは関係なく、ViewHolder と関係があります。インフレーションは ViewHolder で発生します。

  1. onCreateViewHolder() で、関数の本体にあるすべてのコードを選択します。
  2. 右クリックして、[Refactor] > [Extract] > [Function] を選択します。
  3. 関数に from という名前を付け、推奨されるパラメータを受け入れます。[OK] をクリックします。
  4. 関数名 from にカーソルを置きます。Alt+Enter(Mac の場合は Option+Enter)キーを押して、インテンション メニューを開きます。
  5. [コンパニオン オブジェクトに移動] を選択します。from() 関数は、ViewHolder インスタンスではなく ViewHolder クラスで呼び出せるように、コンパニオン オブジェクトに存在する必要があります。
  6. companion オブジェクトを ViewHolder クラスに移動します。
  7. from() を公開しました。
  8. onCreateViewHolder() で、return ステートメントを変更して、ViewHolder クラスの from() の呼び出し結果を返すようにします。

    完成した onCreateViewHolder() メソッドと from() メソッドは次のようになります。コードはエラーなくビルドして実行できるはずです。
    override fun onCreateViewHolder(parent: ViewGroup, viewType: 
Int): ViewHolder {
        return ViewHolder.from(parent)
    }
companion object {
   fun from(parent: ViewGroup): ViewHolder {
       val layoutInflater = LayoutInflater.from(parent.context)
       val view = layoutInflater
               .inflate(R.layout.list_item_sleep_night, parent, false)
       return ViewHolder(view)
   }
}
  1. コンストラクタが非公開になるように、ViewHolder クラスのシグネチャを変更します。from() が新しい ViewHolder インスタンスを返すメソッドになったため、ViewHolder のコンストラクタを呼び出す理由がなくなりました。
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. アプリを実行します。アプリは以前と同じようにビルドして実行されるはずです。これは、リファクタリング後の望ましい結果です。

Android Studio プロジェクト: RecyclerViewFundamentals

  • データのリストやグリッドを表示することは、Android の UI タスクとしては非常に一般的なものです。RecyclerView は、非常に大きなリストを表示する場合でも効率的に動作するように設計されています。
  • RecyclerView は、現時点で画面に表示されるアイテムの処理や描画に必要な作業のみを行います。
  • アイテムが画面外にスクロールされると、そのビューはリサイクルされます。具体的には、画面内にスクロールしてくる新たなコンテンツがその項目に設定されます。
  • ソフトウェア エンジニアリングのアダプター パターンは、オブジェクトが別の API と連携するのに役立ちます。RecyclerView は、アダプタを使用して、アプリのデータの保存方法や処理方法を変更することなく、アプリのデータを表示可能な形式に変換します。

RecyclerView にデータを表示するには、次の部分が必要です。

  • RecyclerView
    RecyclerView のインスタンスを作成するには、レイアウト ファイルで <RecyclerView> 要素を定義します。
  • LayoutManager
    RecyclerViewLayoutManager を使用して、RecyclerView 内のアイテムのレイアウトを整理します(グリッドや線形リストでのレイアウトなど)。

    レイアウト ファイルの <RecyclerView> で、app:layoutManager 属性をレイアウト マネージャー(LinearLayoutManagerGridLayoutManager など)に設定します。

    RecyclerViewLayoutManager をプログラムで設定することもできます。(この手法については、後の Codelab で説明します)。
  • 各アイテムのレイアウト
    XML レイアウト ファイルで 1 つのデータアイテムのレイアウトを作成します。
  • Adapter
    データを準備し、ViewHolder での表示方法を定義するアダプタを作成します。アダプタを RecyclerView に関連付けます。

    RecyclerView が実行されると、アダプタを使用して、データを画面にどのように表示するかを決定します。

    アダプタでは、次のメソッドを実装する必要があります。
    – アイテムの数を返す getItemCount()
    – リスト内のアイテムの ViewHolder を返す onCreateViewHolder()
    – リスト内のアイテムのビューにデータを適応させる onBindViewHolder()

  • ViewHolder
    ViewHolder には、アイテムのレイアウトから 1 つのアイテムを表示するためのビュー情報が含まれています。
  • アダプターの onBindViewHolder() メソッドは、データをビューに適合させます。このメソッドは常にオーバーライドします。通常、onBindViewHolder() はアイテムのレイアウトを拡張し、レイアウト内のビューにデータを配置します。
  • RecyclerView はデータについて何も知らないため、Adapter はデータが変更されたときに RecyclerView に通知する必要があります。notifyDataSetChanged() を使用して、データが変更されたことを Adapter に通知します。

Udacity コース:

Android デベロッパー ドキュメント:

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

以下の質問に回答してください

問題 1

RecyclerView はアイテムをどのように表示しますか。該当するものをすべて選択してください。

▢ リストまたはグリッドにアイテムを表示する。

▢ 縦または横にスクロールする。

▢ タブレットなどの大きなデバイスでは斜めにスクロールする。

▢ リストやグリッドで不十分な場合はカスタム レイアウトを使用できる。

問題 2

RecyclerView を使用するメリットは何ですか。該当するものをすべて選択してください。

▢ 大きなリストを効率よく表示する。

▢ データを自動的に更新する。

▢ アイテムの更新、削除、またはリストへの追加の際に更新する必要性を最小限にする。

▢ 画面外にスクロールされるビューを再利用して、画面内にスクロールされる次のアイテムを表示します。

問題 3

アダプターを使用するメリットは何ですか。該当するものをすべて選択してください。

▢ 「関心の分離」によって、コードの変更とテストを容易にする。

RecyclerView は表示されているデータに依存しません。

▢ データ処理レイヤをデータの表示方法から分離できる。

▢ アプリの動作が高速になる。

問題 4

ViewHolder について、正しいものは次のうちどれですか。該当するものをすべて選択してください。

ViewHolder レイアウトは XML レイアウト ファイルで定義されます。

▢ データセット内のデータのユニットごとに ViewHolder が 1 つあります。

RecyclerView 内の ViewHolder は複数あっても構いません。

Adapter はデータを ViewHolder にバインドします。

次のレッスンに進む: 7.2: DiffUtil および RecyclerView とのデータ バインディング