この 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 から始まるインデックス番号に対応します。次に例を示します。
[実際のデータ] -> [アダプタビュー]
[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 アプリを引き続き使用することができます。
- 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
という単一の値を取ります。これをシールクラスの一部にするには、DataItem
を拡張します。
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- 2 つ目のクラスは
Header
で、ヘッダーを表します。ヘッダーには実際のデータがないため、object
として宣言できます。つまり、Header
のインスタンスは常に 1 つだけになります。ここでも、DataItem
を拡張します。
object Header: DataItem()
DataItem
内のクラスレベルで、id
という名前のabstract
Long
プロパティを定義します。アダプターがDiffUtil
を使用してアイテムが変更されたかどうか、またどのように変更されたかを判別する場合、DiffItemCallback
は各アイテムの ID を認識する必要があります。SleepNightItem
とHeader
は抽象プロパティid
をオーバーライドする必要があるため、エラーが表示されます。
abstract val id: Long
SleepNightItem
で、id
をオーバーライドしてnightId
を返します。
override val id = sleepNight.nightId
Header
でid
をオーバーライドして、Long.MIN_VALUE
は非常に小さい数値(63 の累乗)を返します。したがって、これは実在する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
の宣言を更新する必要があります。ViewHolder
を 1 つだけサポートするのではなく、任意のタイプのビューホルダーを使用できるようにする必要があります。
アイテムのタイプを定義する
SleepNightAdapter.kt
で、トップレベルのimport
ステートメントとSleepNightAdapter
の上に、ビュータイプの定数を 2 つ定義します。RecyclerView
は、ビューホルダーを正しく割り当てることができるように、各アイテムのビュータイプを区別する必要があります。
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 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 定義を更新する
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.ViewHolder
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() を更新する
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 コールバックを更新する
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
}
}
ヘッダーを追加して送信する
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
は、ヘッダーをトラッキングする役割を担います。 - ヘッダーを追加する別の方法として、データグリッドのバッキング データセット(リスト)を変更する方法があります。これはこの 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
を受け入れます。
次のレッスンを開始する:
このコースの他の Codelab へのリンクについては、Android Kotlin の基礎 Codelab ランディング ページをご覧ください。