この 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
という名前のabstract
Long
プロパティを定義します。アダプタがDiffUtil
を使用してアイテムが変更されたかどうか、どのように変更されたかを判断する場合、DiffItemCallback
は各アイテムの ID を知る必要があります。SleepNightItem
とHeader
は抽象プロパティid
をオーバーライドする必要があるため、エラーが表示されます。
abstract val id: Long
SleepNightItem
で、id
をオーバーライドしてnightId
を返します。
override val id = sleepNight.nightId
Header
で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 = 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 コールバックを更新する
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 のランディング ページをご覧ください。