Android Kotlin の基礎 07.5: RecyclerView のヘッダー

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

はじめに

この Codelab では、RecyclerView に表示されるリストの幅全体にわたるヘッダーを追加する方法を学びます。以前の Codelab で作成した睡眠トラッカー アプリを基に作成します。

前提となる知識

  • アクティビティ、フラグメント、ビューを使用して基本的なユーザー インターフェースを構築する方法。
  • フラグメント間を移動する方法、safeArgs を使用してフラグメント間でデータを渡す方法。
  • モデル、モデル ファクトリ、変換、LiveData、オブザーバーを表示します。
  • Room データベースを作成し、DAO を作成して、エンティティを定義する方法。
  • データベース インタラクションやその他の長時間実行タスクにコルーチンを使用する方法。
  • AdapterViewHolder、アイテム レイアウトを使用して基本的な RecyclerView を実装する方法。
  • RecyclerView のデータ バインディングを実装する方法。
  • バインディング アダプタを作成してデータ変換に使用する方法。
  • GridLayoutManager の使用方法。
  • RecyclerView. 内のアイテムのクリックをキャプチャして処理する方法

学習内容

  • RecyclerView で複数の ViewHolder を使用して、異なるレイアウトのアイテムを追加する方法。具体的には、2 つ目の ViewHolder を使用して、RecyclerView に表示される項目の上にヘッダーを追加する方法です。

演習内容

  • このシリーズの前の Codelab で作成した TrackMySleepQuality アプリをベースに構築します。
  • RecyclerView に表示される睡眠日数の上に、画面の幅いっぱいに広がるヘッダーを追加します。

この睡眠トラッカー アプリは、下の図に示すように、フラグメントで表される 3 つの画面で構成されています。

左側に表示されている最初の画面には、トラッキングの開始と停止のボタンがあります。画面には、ユーザーの睡眠データの一部が表示されます。[消去] ボタンをクリックすると、アプリがユーザーのために収集したすべてのデータが完全に削除されます。中央に表示されている 2 つ目の画面は、睡眠の質の評価を選択するためのものです。3 つ目の画面は、ユーザーがグリッド内のアイテムをタップしたときに開く詳細ビューです。

このアプリは、UI コントローラ、ビューモデル、LiveDataRoom データベースを備えた簡素化されたアーキテクチャを使用して、睡眠データを永続化します。

この Codelab では、表示されるアイテムのグリッドにヘッダーを追加します。最終的なメイン画面は次のようになります。

この Codelab では、さまざまなレイアウトを使用するアイテムを RecyclerView に含める一般的な原則について説明します。一般的な例としては、リストやグリッドにヘッダーがある場合が挙げられます。リストには、アイテムの内容を説明するヘッダーを 1 つだけ含めることができます。リストには複数のヘッダーを設定して、1 つのリスト内のアイテムをグループ化したり、区切ったりすることもできます。

RecyclerView は、データや各アイテムのレイアウトの種類を認識しません。LayoutManager は画面上のアイテムを配置しますが、アダプターは表示するデータを調整し、ビューホルダーを RecyclerView に渡します。そのため、アダプターにヘッダーを作成するコードを追加します。

ヘッダーを追加する 2 つの方法

RecyclerView では、リスト内の各項目は 0 から始まるインデックス番号に対応しています。次に例を示します。

[Actual Data] -> [Adapter Views]

[0: SleepNight] -> [0: SleepNight]

[1: SleepNight] -> [1: SleepNight]

[2: SleepNight] -> [2: SleepNight]

リストにヘッダーを追加する方法の 1 つは、ヘッダーを表示する必要があるインデックスをチェックして、別個の ViewHolder を使用するようにアダプタを変更することです。Adapter は、ヘッダーの追跡を担当します。たとえば、テーブルの上部にヘッダーを表示するには、0 から始まるインデックスのアイテムをレイアウトしながら、ヘッダー用に別の ViewHolder を返す必要があります。他のすべての項目は、以下に示すように、ヘッダー オフセットでマッピングされます。

[Actual Data] -> [Adapter Views]

[0: ヘッダー]

[0: SleepNight] -> [1: SleepNight]

[1: SleepNight] -> [2: SleepNight]

[2: SleepNight] -> [3: SleepNight.

ヘッダーを追加する別の方法として、データグリッドのバッキング データセットを変更する方法があります。表示する必要があるデータはすべてリストに保存されているため、リストを変更してヘッダーを表すアイテムを含めることができます。これは少し理解しやすいですが、さまざまなアイテムタイプを 1 つのリストに結合できるように、オブジェクトの設計方法を検討する必要があります。このように実装すると、アダプタは渡されたアイテムを表示します。したがって、位置 0 のアイテムはヘッダーで、位置 1 のアイテムは SleepNight です。これは画面上のものに直接マッピングされます。

[Actual Data] -> [Adapter Views]

[0: ヘッダー] -> [0: ヘッダー]

[1: SleepNight] -> [1: SleepNight]

[2: SleepNight] -> [2: SleepNight]

[3: SleepNight] -> [3: SleepNight]

各方法論にはメリットとデメリットがあります。データセットを変更しても、アダプター コードの残りの部分には大きな変更は加えられません。データのリストを操作して、ヘッダー ロジックを追加できます。一方、ヘッダーのインデックスをチェックして別の ViewHolder を使用すると、ヘッダーのレイアウトの自由度が高まります。また、バッキング データを変更せずに、データをビューに適合させる方法をアダプターで処理することもできます。

この Codelab では、リストの先頭にヘッダーを表示するように RecyclerView を更新します。この場合、アプリはヘッダーとデータアイテムで異なる ViewHolder を使用します。アプリはリストのインデックスをチェックして、使用する ViewHolder を決定します。

ステップ 1: DataItem クラスを作成する

アイテムの型を抽象化して、アダプタが「アイテム」のみを処理できるようにするには、SleepNight または Header を表すデータホルダー クラスを作成します。データセットは、データホルダー項目のリストになります。

GitHub からスターター アプリを取得するか、前回の Codelab で作成した SleepTracker アプリを引き続き使用します。

  1. GitHub から RecyclerViewHeaders-Starter コードをダウンロードします。RecyclerViewHeaders-Starter ディレクトリには、この Codelab に必要な SleepTracker アプリのスターター バージョンが含まれています。必要に応じて、前の Codelab で完成させたアプリをそのまま使用することもできます。
  2. SleepNightAdapter.kt を開きます。
  3. SleepNightListener クラスの下の最上位レベルで、データ項目を表す DataItem という名前の sealed クラスを定義します。

    sealed クラスはクローズド型を定義します。つまり、DataItem のすべてのサブクラスをこのファイルで定義する必要があります。その結果、コンパイラはサブクラスの数を認識します。コードの別の部分で、アダプターを壊す可能性のある新しいタイプの DataItem を定義することはできません。
sealed class DataItem {

 }
  1. DataItem クラスの本体内で、さまざまな種類のデータ項目を表す 2 つのクラスを定義します。1 つ目は SleepNightItem で、SleepNight のラッパーであるため、sleepNight という 1 つの値を取ります。シールクラスの一部にするには、DataItem を拡張します。
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. 2 番目のクラスは Header で、ヘッダーを表します。ヘッダーには実際のデータがないため、object として宣言できます。つまり、Header のインスタンスは常に 1 つだけになります。ここでも、DataItem を拡張します。
object Header: DataItem()
  1. DataItem 内のクラスレベルで、id という名前の abstract Long プロパティを定義します。アダプタが DiffUtil を使用してアイテムが変更されたかどうか、どのように変更されたかを判断する場合、DiffItemCallback は各アイテムの ID を知る必要があります。SleepNightItemHeader は抽象プロパティ id をオーバーライドする必要があるため、エラーが表示されます。
abstract val id: Long
  1. SleepNightItem で、id をオーバーライドして nightId を返します。
override val id = sleepNight.nightId
  1. Headerid をオーバーライドして、非常に小さい数値(-2 の 63 乗)である Long.MIN_VALUE を返します。そのため、既存の nightId と競合することはありません。
override val id = Long.MIN_VALUE
  1. 完成したコードは次のようになります。アプリはエラーなしでビルドされるはずです。
sealed class DataItem {
    abstract val id: Long
    data class SleepNightItem(val sleepNight: SleepNight): DataItem()      {
        override val id = sleepNight.nightId
    }

    object Header: DataItem() {
        override val id = Long.MIN_VALUE
    }
}

ステップ 2: ヘッダーの ViewHolder を作成する

  1. TextView を表示する header.xml という新しいレイアウト リソース ファイルに、ヘッダーのレイアウトを作成します。このコードは特に難しいものではありません。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Sleep Results"
    android:padding="8dp" />
  1. "Sleep Results" を文字列リソースに抽出し、header_text と呼びます。
<string name="header_text">Sleep Results</string>
  1. SleepNightAdapter.ktSleepNightAdapter 内、ViewHolder クラスの上に、新しい TextViewHolder クラスを作成します。このクラスは textview.xml レイアウトを拡張し、TextViewHolder インスタンスを返します。これは以前に行ったことがあるため、コードは次のとおりです。ViewR をインポートする必要があります。
    class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
        companion object {
            fun from(parent: ViewGroup): TextViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val view = layoutInflater.inflate(R.layout.header, parent, false)
                return TextViewHolder(view)
            }
        }
    }

ステップ 3: SleepNightAdapter を更新する

次に、SleepNightAdapter の宣言を更新する必要があります。1 種類の ViewHolder のみをサポートするのではなく、任意の種類のビューホルダーを使用できるようにする必要があります。

アイテムのタイプを定義する

  1. SleepNightAdapter.kt のトップレベルで、import ステートメントの下、SleepNightAdapter の上に、ビュータイプの 2 つの定数を定義します。

    RecyclerView は、各アイテムのビュータイプを区別して、ビューホルダーを正しく割り当てる必要があります。
    private val ITEM_VIEW_TYPE_HEADER = 0
    private val ITEM_VIEW_TYPE_ITEM = 1
  1. SleepNightAdapter 内で、現在のアイテムのタイプに応じて正しいヘッダーまたはアイテムの定数を返すように getItemViewType() をオーバーライドする関数を作成します。
override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
            is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
        }
    }

SleepNightAdapter の定義を更新する

  1. SleepNightAdapter の定義で、ListAdapter の最初の引数を SleepNight から DataItem に更新します。
  2. SleepNightAdapter の定義で、ListAdapter の 2 番目の汎用引数を SleepNightAdapter.ViewHolder から RecyclerView.ViewHolder に変更します。必要な更新に関するエラーが表示されます。クラス ヘッダーは次のようになります。
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

onCreateViewHolder() を更新する

  1. RecyclerView.ViewHolder を返すように onCreateViewHolder() のシグネチャを変更します。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
  1. onCreateViewHolder() メソッドの実装を拡張して、各アイテムタイプに適したビューホルダーをテストして返すようにします。更新されたメソッドは次のようになります。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
            ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
            else -> throw ClassCastException("Unknown viewType ${viewType}")
        }
    }

onBindViewHolder() を更新する

  1. onBindViewHolder() のパラメータの型を ViewHolder から RecyclerView.ViewHolder に変更します。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
  1. ホルダーが ViewHolder の場合にのみ、ビューホルダーにデータを割り当てる条件を追加します。
        when (holder) {
            is ViewHolder -> {...}
  1. getItem() によって返されたオブジェクト型を DataItem.SleepNightItem にキャストします。完成した onBindViewHolder() 関数は次のようになります。
  override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ViewHolder -> {
                val nightItem = getItem(position) as DataItem.SleepNightItem
                holder.bind(nightItem.sleepNight, clickListener)
            }
        }
    }

diffUtil コールバックを更新する

  1. SleepNightDiffCallback のメソッドを変更して、SleepNight の代わりに新しい DataItem クラスを使用します。次のコードに示すように、lint 警告を抑制します。
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
    override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
        return oldItem.id == newItem.id
    }
    @SuppressLint("DiffUtilEquals")
    override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
        return oldItem == newItem
    }
}

ヘッダーを追加して送信する

  1. SleepNightAdapter 内の onCreateViewHolder() の下に、次のように関数 addHeaderAndSubmitList() を定義します。この関数は SleepNight のリストを受け取ります。ListAdapter によって提供される submitList() を使用してリストを送信する代わりに、この関数を使用してヘッダーを追加してからリストを送信します。
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. addHeaderAndSubmitList() 内で、渡されたリストが null の場合はヘッダーのみを返し、それ以外の場合はヘッダーをリストの先頭に追加してからリストを送信します。
val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
submitList(items)
  1. SleepTrackerFragment.kt を開き、submitList() の呼び出しを addHeaderAndSubmitList() に変更します。
  1. アプリを実行し、睡眠アイテムのリストの最初のアイテムとしてヘッダーが表示されることを確認します。

このアプリには 2 つの修正点があります。1 つは目に見えるもので、もう 1 つは目に見えないものです。

  • ヘッダーは左上に表示され、区別しにくい。
  • ヘッダーが 1 つの短いリストではあまり問題になりませんが、UI スレッドの addHeaderAndSubmitList() でリスト操作を行うべきではありません。何百ものアイテム、複数のヘッダー、アイテムを挿入する場所を決定するロジックを含むリストを考えてみましょう。この処理はコルーチンに属します。

コルーチンを使用するように addHeaderAndSubmitList() を変更します。

  1. SleepNightAdapter クラスの最上位レベルで、Dispatchers.Default を使用して CoroutineScope を定義します。
private val adapterScope = CoroutineScope(Dispatchers.Default)
  1. addHeaderAndSubmitList() で、adapterScope のコルーチンを起動してリストを操作します。その後、次のコードに示すように、Dispatchers.Main コンテキストに切り替えてリストを送信します。
 fun addHeaderAndSubmitList(list: List<SleepNight>?) {
        adapterScope.launch {
            val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
            withContext(Dispatchers.Main) {
                submitList(items)
            }
        }
    }
  1. コードはビルドされて実行されますが、違いは確認できません。

現在、ヘッダーはグリッド上の他のアイテムと同じ幅で、水平方向と垂直方向に 1 つのスパンを占有しています。グリッド全体に 1 スパン幅のアイテムが 3 つ横に収まるため、ヘッダーは横方向に 3 スパンを使用する必要があります。

ヘッダーの幅を固定するには、すべての列にデータをまたがらせるタイミングを GridLayoutManager に伝える必要があります。これを行うには、GridLayoutManagerSpanSizeLookup を構成します。これは、GridLayoutManager がリスト内の各アイテムに使用するスパンの数を決定するために使用する構成オブジェクトです。

  1. SleepTrackerFragment.kt を開きます。
  2. onCreateView() の末尾付近で、manager を定義しているコードを探します。
val manager = GridLayoutManager(activity, 3)
  1. 以下に示すように、manager の下に manager.spanSizeLookup を定義します。setSpanSizeLookup はラムダを受け取らないため、object を作成する必要があります。Kotlin で object を作成するには、object : classname(この場合は GridLayoutManager.SpanSizeLookup)と入力します。
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. コンストラクタを呼び出すコンパイラ エラーが発生する可能性があります。その場合は、Option+Enter(Mac)または Alt+Enter(Windows)でインテンション メニューを開き、コンストラクタ呼び出しを適用します。
  1. 次に、メソッドをオーバーライドする必要があるというエラーが object で発生します。カーソルを object に置き、Option+Enter(Mac)または Alt+Enter(Windows)を押してインテンション メニューを開き、メソッド getSpanSize() をオーバーライドします。
  1. getSpanSize() の本体で、各位置の正しいスパンサイズを返します。位置 0 のスパンサイズは 3 で、他の位置のスパンサイズは 1 です。完成したコードは次のようになります。
    manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
            override fun getSpanSize(position: Int) =  when (position) {
                0 -> 3
                else -> 1
            }
        }
  1. ヘッダーの見栄えを良くするには、header.xml を開き、次のコードをレイアウト ファイル header.xml に追加します。
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
  1. アプリを実行します。下のスクリーンショットのようになります。

これで、これで完了です。

Android Studio プロジェクト: RecyclerViewHeaders

  • ヘッダーは通常、リストの幅全体にまたがる項目で、タイトルまたは区切り文字として機能します。リストには、アイテムの内容を説明するヘッダーを 1 つだけ含めることも、アイテムをグループ化してアイテム同士を区切る複数のヘッダーを含めることもできます。
  • RecyclerView は、複数のビューホルダーを使用して、ヘッダーやリスト アイテムなど、さまざまなアイテムに対応できます。
  • ヘッダーを追加する方法の 1 つは、ヘッダーを表示する必要があるインデックスをチェックして、別の ViewHolder を使用するようにアダプターを変更することです。Adapter は、ヘッダーの追跡を担当します。
  • ヘッダーを追加するもう 1 つの方法は、データグリッドのバッキング データセット(リスト)を変更することです。これは、この Codelab で行ったことです。

ヘッダーを追加する主な手順は次のとおりです。

  • ヘッダーまたはデータを保持できる DataItem を作成して、リスト内のデータを抽象化します。
  • アダプターのヘッダーのレイアウトを含むビューホルダーを作成します。
  • あらゆる種類の RecyclerView.ViewHolder を使用するように、アダプターとそのメソッドを更新します。
  • onCreateViewHolder() で、データ項目の正しいタイプのビュー ホルダーを返します。
  • DataItem クラスと連携するように SleepNightDiffCallback を更新します。
  • コルーチンを使用してデータセットにヘッダーを追加し、submitList() を呼び出す addHeaderAndSubmitList() 関数を作成します。
  • GridLayoutManager.SpanSizeLookup() を実装して、ヘッダーのみを 3 つの幅にします。

Udacity コース:

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

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

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

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

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

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

問題 1

ViewHolder の説明として正しいものは次のうちどれですか。

▢ アダプターは、複数の ViewHolder クラスを使用してヘッダーと複数の型のデータを保持できる。

▢ ビューホルダーは、データ用に 1 つと、1 ヘッダーにつき 1 つを設定できる。

RecyclerView は複数の種類のヘッダーをサポートしているが、データの型は統一する必要がある。

▢ ヘッダーを追加する場合は、RecyclerView をサブクラス化してヘッダーを適切な位置に挿入する。

問題 2

RecyclerView でコルーチンを使用するのはどのような場合ですか?正しい記述をすべて選択してください。

▢ なし。RecyclerView は UI 要素であり、コルーチンを使用すべきではありません。

▢ UI の動作を遅くする可能性のある長時間実行タスクにはコルーチンを使用します。

▢ リスト操作には時間がかかることがあるため、常にコルーチンを使用して行う必要があります。

▢ コルーチンと中断関数を使用して、メインスレッドがブロックされないようにします。

問題 3

複数の ViewHolder を使用する場合に、行う必要がないのは次のうちどれですか?

ViewHolder で、必要に応じて複数のレイアウト ファイルを指定して拡張します。

onCreateViewHolder() で、データ項目の正しいタイプのビューホルダーを返します。

onBindViewHolder() では、ビュー ホルダーがデータ項目の正しいタイプのビュー ホルダーである場合にのみ、データをバインドします。

▢ 任意 RecyclerView.ViewHolder を受け入れるようにアダプター クラスのシグネチャを一般化します。

次のレッスンに進む: 8.1 インターネットからデータを取得する

このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。