この 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の宣言のすぐ上に、新しいnavigateToSleepDetailLiveDataを監視するコードを追加します。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 のレイアウト ファイルに追加する。
次のレッスンに進む:

