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 コントローラ、ビューモデル、LiveData を含むシンプルなアーキテクチャ、および睡眠データを保持するための Room データベースを使用します。

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

この Codelab では、異なるレイアウトを使用するアイテムを RecyclerView に含めるという原則について説明します。たとえば、リストやグリッドにヘッダーを含めることができます。リストには、アイテムの内容を説明するヘッダーを 1 つ含めることができます。リストには、項目を 1 つにまとめた複数のヘッダーを指定することもできます。

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

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

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

[実際のデータ] -> [アダプタビュー]

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

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

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

リストにヘッダーを追加する方法の 1 つは、ヘッダーを表示する必要があるインデックスを確認して、別の ViewHolder を使用するようにアダプターを変更することです。Adapter がヘッダーをトラッキングします。たとえば、表の上部にヘッダーを表示するには、そのヘッダーに対して異なる ViewHolder を返し、ゼロ インデックスの項目をレイアウトする必要があります。次に、他のすべてのアイテムは、以下に示すようにヘッダー オフセットでマッピングされます。

[実際のデータ] -> [アダプタビュー]

[0: ヘッダー]

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

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

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

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

[実際のデータ] -> [アダプタビュー]

[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 という単一の値を取ります。これをシールクラスの一部にするには、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 をオーバーライドして、Long.MIN_VALUE は非常に小さい数値(63 の累乗)を返します。したがって、これは実在する 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 の宣言を更新する必要があります。ViewHolder を 1 つだけサポートするのではなく、任意のタイプのビューホルダーを使用できるようにする必要があります。

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

  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. SleepNight ではなく新しい DataItem クラスを使用するように、SleepNightDiffCallback のメソッドを変更します。下記のコードに示すように、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. SleepNightAdapteronCreateViewHolder() の下で、次のように関数 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. 次に示すように、managermanager.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 は、ヘッダーをトラッキングする役割を担います。
  • ヘッダーを追加する別の方法として、データグリッドのバッキング データセット(リスト)を変更する方法があります。これはこの Codelab で行ったものです。

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

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

Udacity コース:

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

このセクションでは、インストラクターが主導するコースの一環として、この Codelab に取り組む生徒の課題について説明します。教師は以下のことを行えます。

  • 必要に応じて課題を割り当てます。
  • 宿題の提出方法を生徒に伝える。
  • 宿題を採点します。

教師はこれらの提案を少しだけ使うことができます。また、他の課題は自由に割り当ててください。

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

次の質問に答えてください。

問題 1

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

▢ アダプターは複数の ViewHolder クラスを使用して、ヘッダーやさまざまな種類のデータを保持できます。

▢ データについてはビューホルダーを 1 つ、ヘッダーについてはビューホルダーを 1 つだけ持つことができます。

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

▢ ヘッダーを追加する際には RecyclerView をサブクラス化して、ヘッダーを正しい位置に挿入します。

質問 2

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

▢ 認証は行わないRecyclerView は UI 要素であるため、コルーチンを使用しないでください。

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

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

▢ メイン関数をブロックしないように、suspend 関数とともにコルーチンを使用する。

問題 3

次のうち、複数の ViewHolder を使用する必要のないものはどれですか?

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

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

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

▢ アダプター クラスの署名を一般化して、任意の RecyclerView.ViewHolder を受け入れます。

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

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