この Codelab は、Android Kotlin の基礎コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。コースのすべての Codelab は、Android Kotlin の基礎の Codelab のランディング ページに一覧表示されています。
はじめに
この Codelab では、RecyclerView に表示されるリストの幅全体にわたるヘッダーを追加する方法を学びます。以前の Codelab で作成した睡眠トラッカー アプリを基に作成します。
前提となる知識
- アクティビティ、フラグメント、ビューを使用して基本的なユーザー インターフェースを構築する方法。
- フラグメント間を移動する方法、
safeArgsを使用してフラグメント間でデータを渡す方法。 - モデル、モデル ファクトリ、変換、
LiveData、オブザーバーを表示します。 Roomデータベースを作成し、DAO を作成して、エンティティを定義する方法。- データベース インタラクションやその他の長時間実行タスクにコルーチンを使用する方法。
Adapter、ViewHolder、アイテム レイアウトを使用して基本的な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 から始まるインデックス番号に対応しています。次に例を示します。
[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 アプリを引き続き使用します。
- GitHub から RecyclerViewHeaders-Starter コードをダウンロードします。RecyclerViewHeaders-Starter ディレクトリには、この Codelab に必要な SleepTracker アプリのスターター バージョンが含まれています。必要に応じて、前の Codelab で完成させたアプリをそのまま使用することもできます。
- SleepNightAdapter.kt を開きます。
SleepNightListenerクラスの下の最上位レベルで、データ項目を表すDataItemという名前のsealedクラスを定義します。sealedクラスはクローズド型を定義します。つまり、DataItemのすべてのサブクラスをこのファイルで定義する必要があります。その結果、コンパイラはサブクラスの数を認識します。コードの別の部分で、アダプターを壊す可能性のある新しいタイプのDataItemを定義することはできません。
sealed class DataItem {
}DataItemクラスの本体内で、さまざまな種類のデータ項目を表す 2 つのクラスを定義します。1 つ目はSleepNightItemで、SleepNightのラッパーであるため、sleepNightという 1 つの値を取ります。シールクラスの一部にするには、DataItemを拡張します。
data class SleepNightItem(val sleepNight: SleepNight): DataItem()- 2 番目のクラスは
Headerで、ヘッダーを表します。ヘッダーには実際のデータがないため、objectとして宣言できます。つまり、Headerのインスタンスは常に 1 つだけになります。ここでも、DataItemを拡張します。
object Header: DataItem()DataItem内のクラスレベルで、idという名前のabstractLongプロパティを定義します。アダプタがDiffUtilを使用してアイテムが変更されたかどうか、どのように変更されたかを判断する場合、DiffItemCallbackは各アイテムの ID を知る必要があります。SleepNightItemとHeaderは抽象プロパティidをオーバーライドする必要があるため、エラーが表示されます。
abstract val id: LongSleepNightItemで、idをオーバーライドしてnightIdを返します。
override val id = sleepNight.nightIdHeaderでidをオーバーライドして、非常に小さい数値(-2 の 63 乗)であるLong.MIN_VALUEを返します。そのため、既存のnightIdと競合することはありません。
override val id = Long.MIN_VALUE- 完成したコードは次のようになります。アプリはエラーなしでビルドされるはずです。
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 を作成する
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" />"Sleep Results"を文字列リソースに抽出し、header_textと呼びます。
<string name="header_text">Sleep Results</string>- SleepNightAdapter.kt の
SleepNightAdapter内、ViewHolderクラスの上に、新しいTextViewHolderクラスを作成します。このクラスは textview.xml レイアウトを拡張し、TextViewHolderインスタンスを返します。これは以前に行ったことがあるため、コードは次のとおりです。ViewとRをインポートする必要があります。
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 のみをサポートするのではなく、任意の種類のビューホルダーを使用できるようにする必要があります。
アイテムのタイプを定義する
SleepNightAdapter.ktのトップレベルで、importステートメントの下、SleepNightAdapterの上に、ビュータイプの 2 つの定数を定義します。RecyclerViewは、各アイテムのビュータイプを区別して、ビューホルダーを正しく割り当てる必要があります。
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 1SleepNightAdapter内で、現在のアイテムのタイプに応じて正しいヘッダーまたはアイテムの定数を返すように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 の定義を更新する
SleepNightAdapterの定義で、ListAdapterの最初の引数をSleepNightからDataItemに更新します。SleepNightAdapterの定義で、ListAdapterの 2 番目の汎用引数をSleepNightAdapter.ViewHolderからRecyclerView.ViewHolderに変更します。必要な更新に関するエラーが表示されます。クラス ヘッダーは次のようになります。
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {onCreateViewHolder() を更新する
RecyclerView.ViewHolderを返すようにonCreateViewHolder()のシグネチャを変更します。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolderonCreateViewHolder()メソッドの実装を拡張して、各アイテムタイプに適したビューホルダーをテストして返すようにします。更新されたメソッドは次のようになります。
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() を更新する
onBindViewHolder()のパラメータの型をViewHolderからRecyclerView.ViewHolderに変更します。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)- ホルダーが
ViewHolderの場合にのみ、ビューホルダーにデータを割り当てる条件を追加します。
when (holder) {
is ViewHolder -> {...}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 コールバックを更新する
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
}
}ヘッダーを追加して送信する
SleepNightAdapter内のonCreateViewHolder()の下に、次のように関数addHeaderAndSubmitList()を定義します。この関数はSleepNightのリストを受け取ります。ListAdapterによって提供されるsubmitList()を使用してリストを送信する代わりに、この関数を使用してヘッダーを追加してからリストを送信します。
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}addHeaderAndSubmitList()内で、渡されたリストがnullの場合はヘッダーのみを返し、それ以外の場合はヘッダーをリストの先頭に追加してからリストを送信します。
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
submitList(items)- SleepTrackerFragment.kt を開き、
submitList()の呼び出しをaddHeaderAndSubmitList()に変更します。
- アプリを実行し、睡眠アイテムのリストの最初のアイテムとしてヘッダーが表示されることを確認します。

このアプリには 2 つの修正点があります。1 つは目に見えるもので、もう 1 つは目に見えないものです。
- ヘッダーは左上に表示され、区別しにくい。
- ヘッダーが 1 つの短いリストではあまり問題になりませんが、UI スレッドの
addHeaderAndSubmitList()でリスト操作を行うべきではありません。何百ものアイテム、複数のヘッダー、アイテムを挿入する場所を決定するロジックを含むリストを考えてみましょう。この処理はコルーチンに属します。
コルーチンを使用するように addHeaderAndSubmitList() を変更します。
SleepNightAdapterクラスの最上位レベルで、Dispatchers.Defaultを使用してCoroutineScopeを定義します。
private val adapterScope = CoroutineScope(Dispatchers.Default)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 スパン幅のアイテムが 3 つ横に収まるため、ヘッダーは横方向に 3 スパンを使用する必要があります。
ヘッダーの幅を固定するには、すべての列にデータをまたがらせるタイミングを GridLayoutManager に伝える必要があります。これを行うには、GridLayoutManager で SpanSizeLookup を構成します。これは、GridLayoutManager がリスト内の各アイテムに使用するスパンの数を決定するために使用する構成オブジェクトです。
- SleepTrackerFragment.kt を開きます。
onCreateView()の末尾付近で、managerを定義しているコードを探します。
val manager = GridLayoutManager(activity, 3)- 以下に示すように、
managerの下にmanager.spanSizeLookupを定義します。setSpanSizeLookupはラムダを受け取らないため、objectを作成する必要があります。Kotlin でobjectを作成するには、object : classname(この場合はGridLayoutManager.SpanSizeLookup)と入力します。
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}- コンストラクタを呼び出すコンパイラ エラーが発生する可能性があります。その場合は、
Option+Enter(Mac)またはAlt+Enter(Windows)でインテンション メニューを開き、コンストラクタ呼び出しを適用します。
- 次に、メソッドをオーバーライドする必要があるというエラーが
objectで発生します。カーソルをobjectに置き、Option+Enter(Mac)またはAlt+Enter(Windows)を押してインテンション メニューを開き、メソッドgetSpanSize()をオーバーライドします。
getSpanSize()の本体で、各位置の正しいスパンサイズを返します。位置 0 のスパンサイズは 3 で、他の位置のスパンサイズは 1 です。完成したコードは次のようになります。
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = when (position) {
0 -> 3
else -> 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"- アプリを実行します。下のスクリーンショットのようになります。

これで、これで完了です。
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 を受け入れるようにアダプター クラスのシグネチャを一般化します。
次のレッスンに進む:
このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。


