この Codelab は、Android Kotlin の基礎コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。コースのすべての Codelab は、Android Kotlin の基礎の Codelab のランディング ページに一覧表示されています。
はじめに
アイテムを表示するリストやグリッドを使用するほとんどのアプリでは、ユーザーがアイテムを操作できます。リストからアイテムをタップしてアイテムの詳細を表示することは、このタイプのインタラクションの非常に一般的なユースケースです。これを実現するには、アイテムをタップしたユーザーに詳細ビューを表示するクリック リスナーを追加します。
この Codelab では、前の Codelab シリーズの睡眠トラッカー アプリの拡張版を基に、RecyclerView
にインタラクションを追加します。
前提となる知識
- アクティビティ、フラグメント、ビューを使用して基本的なユーザー インターフェースを構築する。
- フラグメント間の移動、
safeArgs
を使用してフラグメント間でデータを渡す。 - モデル、モデル ファクトリ、変換、
LiveData
、オブザーバーを表示します。 Room
データベースを作成し、データ アクセス オブジェクト(DAO)を作成して、エンティティを定義する方法。- データベースやその他の長時間実行タスクにコルーチンを使用する方法。
Adapter
、ViewHolder
、アイテム レイアウトを使用して基本的なRecyclerView
を実装する方法。RecyclerView
のデータ バインディングを実装する方法。- バインディング アダプタを作成してデータ変換に使用する方法。
GridLayoutManager
の使用方法。
学習内容
RecyclerView
のアイテムをクリック可能にする方法。アイテムがクリックされたときに詳細ビューに移動するクリック リスナーを実装します。
演習内容
- このシリーズの前の Codelab で作成した TrackMySleepQuality アプリの拡張版を基に作成します。
- リストにクリック リスナーを追加し、ユーザー操作のリスニングを開始します。リストアイテムをタップすると、クリックしたアイテムの詳細を含むフラグメントに移動します。スターター コードには、詳細フラグメントのコードとナビゲーション コードが含まれています。
下の図に示すように、睡眠トラッカー アプリの開始画面は、フラグメントで表される 2 つの画面で構成されています。
左側に表示されている最初の画面には、トラッキングの開始と停止のボタンがあります。画面には、ユーザーの睡眠データの一部が表示されます。[消去] ボタンをクリックすると、アプリがユーザーのために収集したすべてのデータが完全に削除されます。右側の 2 つ目の画面は、睡眠の質の評価を選択するための画面です。
このアプリは、UI コントローラ、ビューモデル、LiveData
、Room
データベースを備えた簡素化されたアーキテクチャを使用して、睡眠データを永続化します。
この Codelab では、ユーザーがグリッド内のアイテムをタップしたときに、次のような詳細画面を表示する機能を追加します。この画面のコード(フラグメント、ビューモデル、ナビゲーション)はスターター アプリで提供されるため、クリック処理メカニズムを実装します。
ステップ 1: スターター アプリを入手する
- GitHub から RecyclerViewClickHandler-Starter コードをダウンロードし、Android Studio でプロジェクトを開きます。
- スターターの睡眠トラッカー アプリをビルドして実行します。
[省略可] 前回の Codelab のアプリを使用する場合は、アプリを更新する
この Codelab の GitHub で提供されているスターター アプリを使用する場合は、次のステップに進みます。
前の Codelab で作成した独自の睡眠トラッカー アプリを引き続き使用する場合は、以下の手順に沿って既存のアプリを更新し、詳細画面フラグメントのコードを追加してください。
- 既存のアプリを継続する場合でも、ファイルをコピーできるように、GitHub から RecyclerViewClickHandler-Starter コードを入手してください。
sleepdetail
パッケージ内のすべてのファイルをコピーします。layout
フォルダで、fragment_sleep_detail.xml
ファイルをコピーします。navigation.xml
の更新された内容をコピーします。これにより、sleep_detail_fragment
のナビゲーションが追加されます。database
パッケージのSleepDatabaseDao
に、新しいgetNightWithId()
メソッドを追加します。
/**
* Selects and returns the night with given nightId.
*/
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
fun getNightWithId(key: Long): LiveData<SleepNight>
res/values/strings
に次の文字列リソースを追加します。
<string name="close">Close</string>
- アプリをクリーンアップして再ビルドし、データ バインディングを更新します。
ステップ 2: 睡眠の詳細画面のコードを調べる
この Codelab では、クリックされた睡眠の夜の詳細を表示するフラグメントに移動するクリック ハンドラを実装します。この SleepDetailFragment
のフラグメントとナビゲーション グラフは、コードの量がかなり多く、フラグメントとナビゲーションはこの Codelab の対象外であるため、スターター コードにすでに含まれています。次のコードについてよく理解してください。
- アプリで
sleepdetail
パッケージを見つけます。このパッケージには、1 晩の睡眠の詳細を表示するフラグメントのフラグメント、ビューモデル、ビューモデル ファクトリが含まれています。 sleepdetail
パッケージで、SleepDetailViewModel
のコードを開いて調べます。このビューモデルは、コンストラクタでSleepNight
のキーと DAO を受け取ります。
クラスの本体には、指定されたキーのSleepNight
を取得するコードと、[閉じる] ボタンが押されたときにSleepTrackerFragment
に戻るナビゲーションを制御するnavigateToSleepTracker
変数があります。getNightWithId()
関数はLiveData<SleepNight>
を返し、SleepDatabaseDao
(database
パッケージ内)で定義されています。sleepdetail
パッケージで、SleepDetailFragment
のコードを開いて調べます。データ バインディング、ビューモデル、ナビゲーションのオブザーバーの設定に注目してください。sleepdetail
パッケージで、SleepDetailViewModelFactory
のコードを開いて調べます。- レイアウト フォルダで
fragment_sleep_detail.xml
を調べます。<data>
タグで定義されたsleepDetailViewModel
変数に注目してください。この変数は、各ビューに表示するデータをビューモデルから取得します。
レイアウトには、睡眠の質を表すImageView
、質の評価を表すTextView
、睡眠時間を表すTextView
、詳細フラグメントを閉じるButton
を含むConstraintLayout
が含まれています。 navigation.xml
ファイルを開きます。sleep_tracker_fragment
で、sleep_detail_fragment
の新しいアクションを確認します。
新しいアクションaction_sleep_tracker_fragment_to_sleepDetailFragment
は、睡眠トラッカー フラグメントから詳細画面へのナビゲーションです。
このタスクでは、タップされたアイテムの詳細画面を表示することで、ユーザーのタップに応答するように RecyclerView
を更新します。
クリックを受け取って処理するタスクは 2 つの部分に分かれています。まず、クリックをリッスンして受け取り、どのアイテムがクリックされたかを判断する必要があります。次に、クリックに対してアクションで応答する必要があります。
では、このアプリのクリック リスナーを追加するのに最適な場所はどこでしょうか?
SleepTrackerFragment
は多くのビューをホストしているため、フラグメント レベルでクリック イベントをリッスンしても、どのアイテムがクリックされたかはわかりません。クリックされたのがアイテムなのか、他の UI 要素なのかもわかりません。RecyclerView
レベルでリスニングしている場合、ユーザーがリスト内のどのアイテムをクリックしたかを正確に把握することは困難です。- クリックされた 1 つのアイテムに関する情報を取得する最適なペースは、
ViewHolder
オブジェクトです。これは 1 つのリストアイテムを表すためです。
ViewHolder
はクリックのリッスンには適していますが、クリックを処理する場所としては通常適切ではありません。では、クリックを処理するのに最適な場所はどこでしょうか?
- ビューにデータアイテムを表示するのは
Adapter
であるため、そこでもクリックの処理は可能です。しかし、アダプターの仕事はデータをディスプレイに合わせることであって、アプリのロジックを処理することではありません。 - 通常、クリックの処理は
ViewModel
で行います。ViewModel
は、クリックへの応答として何をすべきか判断するのに必要なデータとロジックにアクセスできるからです。
ステップ 1: クリック リスナーを作成し、アイテム レイアウトからトリガーする
sleeptracker
フォルダで、SleepNightAdapter.kt を開きます。- ファイルの末尾の最上位レベルで、新しいリスナー クラス
SleepNightListener
を作成します。
class SleepNightListener() {
}
SleepNightListener
クラス内にonClick()
関数を追加します。リスト項目を表示するビューがクリックされると、ビューはこのonClick()
関数を呼び出します。(ビューのandroid:onClick
プロパティは、後でこの関数に設定します)。
class SleepNightListener() {
fun onClick() =
}
SleepNight
型の関数引数night
をonClick()
に追加します。ビューは表示しているアイテムを認識しており、クリックを処理するためにその情報を渡す必要があります。
class SleepNightListener() {
fun onClick(night: SleepNight) =
}
onClick()
の動作を定義するには、SleepNightListener
のコンストラクタでclickListener
コールバックを提供し、onClick()
に割り当てます。
クリックを処理するラムダにclickListener
という名前を付けると、クラス間で渡されるときに追跡しやすくなります。clickListener
コールバックは、データベースからデータにアクセスするためにnight.nightId
のみを必要とします。完成したSleepNightListener
クラスは次のようになります。
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
fun onClick(night: SleepNight) = clickListener(night.nightId)
}
- list_item_sleep_night.xml. を開きます。
data
ブロック内で、データ バインディングを通じてSleepNightListener
クラスを使用できるようにする新しい変数を追加します。新しい<variable>
にclickListener.
のname
を指定します。次に示すように、type
をクラスcom.example.android.trackmysleepquality.sleeptracker.SleepNightListener
の完全修飾名に設定します。このレイアウトからSleepNightListener
のonClick()
関数にアクセスできるようになりました。
<variable
name="clickListener"
type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
- このリストアイテムの任意の場所のクリックをリッスンするには、
ConstraintLayout
にandroid:onClick
属性を追加します。
次の例に示すように、データ バインディング ラムダを使用して属性をclickListener:onClick(sleep)
に設定します。
android:onClick="@{() -> clickListener.onClick(sleep)}"
ステップ 2: クリック リスナーをビューホルダーとバインディング オブジェクトに渡す
- SleepNightAdapter.kt を開きます。
val clickListener: SleepNightListener
を受け取るようにSleepNightAdapter
クラスのコンストラクタを変更します。アダプターがViewHolder
をバインドするときに、このクリック リスナーを提供する必要があります。
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
onBindViewHolder()
で、holder.bind()
の呼び出しを更新して、クリック リスナーもViewHolder
に渡します。関数呼び出しにパラメータを追加したため、コンパイラ エラーが発生します。
holder.bind(getItem(position)!!, clickListener)
bind()
にclickListener
パラメータを追加します。これを行うには、下のスクリーンショットに示すように、エラーにカーソルを置き、エラーでAlt+Enter
(Windows)またはOption+Enter
(Mac)を押します。
ViewHolder
クラスのbind()
関数内で、クリック リスナーをbinding
オブジェクトに割り当てます。バインディング オブジェクトを更新する必要があるため、エラーが表示されます。
binding.clickListener = clickListener
- データバインディングを更新するには、プロジェクトをクリーンして再ビルドします。(キャッシュの無効化も必要になる場合があります)。アダプターのコンストラクタからクリック リスナーを取得し、ビューホルダーとバインディング オブジェクトに渡しました。
ステップ 3: 項目がタップされたときにトーストを表示する
クリックをキャプチャするコードは用意できましたが、リストアイテムがタップされたときに何が起こるかは実装していません。最も簡単なレスポンスは、アイテムがクリックされたときに nightId
を示すトーストを表示することです。これにより、リスト項目がクリックされたときに正しい nightId
がキャプチャされて渡されることが検証されます。
- SleepTrackerFragment.kt を開きます。
onCreateView()
でadapter
変数を見つけます。クリック リスナー パラメータが想定されるようになったため、エラーが表示されます。SleepNightAdapter
にラムダを渡して、クリック リスナーを定義します。この単純なラムダは、以下に示すように、nightId
を示すトーストを表示するだけです。Toast
をインポートする必要があります。更新された定義の全文は次のとおりです。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
- アプリを実行し、アイテムをタップして、正しい
nightId
を含むトーストが表示されることを確認します。アイテムのnightId
値は増加しており、アプリは最新の夜を最初に表示するため、nightId
の値が最も低いアイテムがリストの一番下に表示されます。
このタスクでは、RecyclerView
のアイテムがクリックされたときの動作を変更して、トーストを表示する代わりに、クリックされた夜の詳細情報を表示する詳細フラグメントにアプリが移動するようにします。
ステップ 1: クリックで移動する
このステップでは、トーストを表示するだけでなく、SleepTrackerFragment
の onCreateView()
のクリック リスナー ラムダを変更して、nightId
を SleepTrackerViewModel
に渡し、SleepDetailFragment
へのナビゲーションをトリガーします。
クリック ハンドラ関数を定義します。
- SleepTrackerViewModel.kt を開きます。
SleepTrackerViewModel
の最後の方で、onSleepNightClicked()
クリック ハンドラ関数を定義します。
fun onSleepNightClicked(id: Long) {
}
onSleepNightClicked()
の中で、クリックされた睡眠日のid
を_navigateToSleepDetail
に設定してナビゲーションをトリガーします。
fun onSleepNightClicked(id: Long) {
_navigateToSleepDetail.value = id
}
_navigateToSleepDetail
を実装します。これまでと同様に、ナビゲーション状態のprivate MutableLiveData
を定義します。また、それに対応する公開 gettableval
もあります。
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
get() = _navigateToSleepDetail
- アプリのナビゲーションが完了した後に呼び出すメソッドを定義します。
onSleepDetailNavigated()
という名前にして、値をnull
に設定します。
fun onSleepDetailNavigated() {
_navigateToSleepDetail.value = null
}
クリック ハンドラを呼び出すコードを追加します。
- SleepTrackerFragment.kt を開き、アダプターを作成して
SleepNightListener
を定義し、トーストを表示するコードまでスクロールします。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
- 次のコードをトーストの下に追加して、項目がタップされたときに
sleepTrackerViewModel
のクリック ハンドラonSleepNighClicked()
を呼び出します。nightId
を渡して、ビューモデルがどの睡眠夜を取得するかを把握できるようにします。onSleepNightClicked()
はまだ定義されていないため、エラーは表示されたままです。トーストは、必要に応じて保持、コメントアウト、削除できます。
sleepTrackerViewModel.onSleepNightClicked(nightId)
クリックを監視するコードを追加します。
- SleepTrackerFragment.kt を開きます。
onCreateView()
で、manager
の宣言のすぐ上に、新しいnavigateToSleepDetail
LiveData
を監視するコードを追加します。navigateToSleepDetail
が変更されたら、night
を渡してSleepDetailFragment
に移動し、その後onSleepDetailNavigated()
を呼び出します。これは以前の Codelab で行ったことがあるため、コードは次のとおりです。
sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepDetailFragment(night))
sleepTrackerViewModel.onSleepDetailNavigated()
}
})
- コードを実行し、アイテムをクリックすると、アプリがクラッシュします。
バインディング アダプタで null 値を処理します。
- デバッグモードでアプリを再度実行します。項目をタップし、ログをフィルタしてエラーを表示します。次のような内容を含むスタック トレースが表示されます。
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item
残念ながら、スタック トレースではこのエラーがトリガーされた場所が明確に示されていません。データ バインディングの欠点の 1 つは、コードのデバッグが難しくなることです。項目をクリックするとアプリがクラッシュし、新しいコードはクリック処理のみです。
ただし、この新しいクリック処理メカニズムにより、バインディング アダプターが item
の null
値で呼び出されるようになりました。特に、アプリが起動すると LiveData
は null
として起動するため、各アダプタに null チェックを追加する必要があります。
BindingUtils.kt
で、各バインディング アダプタについて、item
引数の型を null 値許容に変更し、本文をitem?.let{...}
でラップします。たとえば、sleepQualityString
のアダプターは次のようになります。他のアダプタも同様に変更します。
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
item?.let {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
}
- アプリを実行します。項目をタップすると、詳細ビューが開きます。
Android Studio プロジェクト: RecyclerViewClickHandler。
RecyclerView
内のアイテムがクリックに応答するようにするには、ViewHolder
内のリストアイテムにクリック リスナーをアタッチし、ViewModel
でクリックを処理します。
RecyclerView
内のアイテムがクリックに応答するようにするには、次の操作を行う必要があります。
- ラムダを受け取り、それを
onClick()
関数に割り当てるリスナー クラスを作成します。
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
fun onClick(night: SleepNight) = clickListener(night.nightId)
}
- ビューにクリック リスナーを設定します。
android:onClick="@{() -> clickListener.onClick(sleep)}"
- クリック リスナーをアダプターのコンストラクタに渡し、ビューホルダーに渡して、バインディング オブジェクトに追加します。
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
- リサイクラー ビューを表示するフラグメントで、アダプターを作成する際に、ラムダをアダプターに渡してクリック リスナーを定義します。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
sleepTrackerViewModel.onSleepNightClicked(nightId)
})
- ビューモデルでクリック ハンドラを実装します。リストアイテムのクリックでは、通常、詳細フラグメントへのナビゲーションがトリガーされます。
Udacity コース:
Android デベロッパー ドキュメント:
このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。
- 必要に応じて宿題を与える
- 宿題の提出方法を生徒に伝える
- 宿題を採点する
インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。
この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。
以下の質問に回答してください
問題 1
アプリに、買い物リストのアイテムを表示する RecyclerView
が含まれているとします。アプリでは、クリック リスナー クラスも定義します。
class ShoppingListItemListener(val clickListener: (itemId: Long) -> Unit) {
fun onClick(cartItem: CartItem) = clickListener(cartItem.itemId)
}
データ バインディングで ShoppingListItemListener
を使用できるようにするにはどうすればよいですか?1 つ選択してください。
▢ 買い物リストを表示する RecyclerView
を含むレイアウト ファイルで、ShoppingListItemListener
の <data>
変数を追加します。
▢ ショッピング リストの 1 行のレイアウトを定義するレイアウト ファイルで、ShoppingListItemListener
の <data>
変数を追加します。
▢ ShoppingListItemListener
クラスで、データ バインディングを有効にする関数を追加します。
fun onBinding (cartItem: CartItem) {dataBindingEnable(true)}
▢ ShoppingListItemListener
クラスの onClick()
関数内で、データ バインディングを有効にする呼び出しを追加します。
fun onClick(cartItem: CartItem) = {
clickListener(cartItem.itemId)
dataBindingEnable(true)
}
問題 2
RecyclerView
内のアイテムがクリックに応答するようにするには、android:onClick
属性をどこに追加しますか。該当するものをすべて選択してください。
▢ RecyclerView
を表示するレイアウト ファイルで <androidx.recyclerview.widget.RecyclerView>
に追加する
▢ 行内のアイテムのレイアウト ファイルに追加する。アイテム全体をクリック可能にするには、親ビューのアイテムの行に追加する。
▢ 行内のアイテムのレイアウト ファイルに追加する。アイテム内の単一の TextView
をクリック可能にする場合は、<TextView>
に追加します。
▢ 常に MainActivity
のレイアウト ファイルに追加する。
次のレッスンに進む: