Android Kotlin の基礎 07.4: RecyclerView アイテムを操作する

この Codelab は、Android Kotlin の基礎コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。コースのすべての Codelab は、Android Kotlin の基礎の Codelab のランディング ページに一覧表示されています。

はじめに

アイテムを表示するリストやグリッドを使用するほとんどのアプリでは、ユーザーがアイテムを操作できます。リストからアイテムをタップしてアイテムの詳細を表示することは、このタイプのインタラクションの非常に一般的なユースケースです。これを実現するには、アイテムをタップしたユーザーに詳細ビューを表示するクリック リスナーを追加します。

この Codelab では、前の Codelab シリーズの睡眠トラッカー アプリの拡張版を基に、RecyclerView にインタラクションを追加します。

前提となる知識

  • アクティビティ、フラグメント、ビューを使用して基本的なユーザー インターフェースを構築する。
  • フラグメント間の移動、safeArgs を使用してフラグメント間でデータを渡す。
  • モデル、モデル ファクトリ、変換、LiveData、オブザーバーを表示します。
  • Room データベースを作成し、データ アクセス オブジェクト(DAO)を作成して、エンティティを定義する方法。
  • データベースやその他の長時間実行タスクにコルーチンを使用する方法。
  • AdapterViewHolder、アイテム レイアウトを使用して基本的な RecyclerView を実装する方法。
  • RecyclerView のデータ バインディングを実装する方法。
  • バインディング アダプタを作成してデータ変換に使用する方法。
  • GridLayoutManager の使用方法。

学習内容

  • RecyclerView のアイテムをクリック可能にする方法。アイテムがクリックされたときに詳細ビューに移動するクリック リスナーを実装します。

演習内容

  • このシリーズの前の Codelab で作成した TrackMySleepQuality アプリの拡張版を基に作成します。
  • リストにクリック リスナーを追加し、ユーザー操作のリスニングを開始します。リストアイテムをタップすると、クリックしたアイテムの詳細を含むフラグメントに移動します。スターター コードには、詳細フラグメントのコードとナビゲーション コードが含まれています。

下の図に示すように、睡眠トラッカー アプリの開始画面は、フラグメントで表される 2 つの画面で構成されています。

左側に表示されている最初の画面には、トラッキングの開始と停止のボタンがあります。画面には、ユーザーの睡眠データの一部が表示されます。[消去] ボタンをクリックすると、アプリがユーザーのために収集したすべてのデータが完全に削除されます。右側の 2 つ目の画面は、睡眠の質の評価を選択するための画面です。

このアプリは、UI コントローラ、ビューモデル、LiveDataRoom データベースを備えた簡素化されたアーキテクチャを使用して、睡眠データを永続化します。

この Codelab では、ユーザーがグリッド内のアイテムをタップしたときに、次のような詳細画面を表示する機能を追加します。この画面のコード(フラグメント、ビューモデル、ナビゲーション)はスターター アプリで提供されるため、クリック処理メカニズムを実装します。

ステップ 1: スターター アプリを入手する

  1. GitHub から RecyclerViewClickHandler-Starter コードをダウンロードし、Android Studio でプロジェクトを開きます。
  2. スターターの睡眠トラッカー アプリをビルドして実行します。

[省略可] 前回の Codelab のアプリを使用する場合は、アプリを更新する

この Codelab の GitHub で提供されているスターター アプリを使用する場合は、次のステップに進みます。

前の Codelab で作成した独自の睡眠トラッカー アプリを引き続き使用する場合は、以下の手順に沿って既存のアプリを更新し、詳細画面フラグメントのコードを追加してください。

  1. 既存のアプリを継続する場合でも、ファイルをコピーできるように、GitHub から RecyclerViewClickHandler-Starter コードを入手してください。
  2. sleepdetail パッケージ内のすべてのファイルをコピーします。
  3. layout フォルダで、fragment_sleep_detail.xml ファイルをコピーします。
  4. navigation.xml の更新された内容をコピーします。これにより、sleep_detail_fragment のナビゲーションが追加されます。
  5. 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>
  1. res/values/strings に次の文字列リソースを追加します。
<string name="close">Close</string>
  1. アプリをクリーンアップして再ビルドし、データ バインディングを更新します。

ステップ 2: 睡眠の詳細画面のコードを調べる

この Codelab では、クリックされた睡眠の夜の詳細を表示するフラグメントに移動するクリック ハンドラを実装します。この SleepDetailFragment のフラグメントとナビゲーション グラフは、コードの量がかなり多く、フラグメントとナビゲーションはこの Codelab の対象外であるため、スターター コードにすでに含まれています。次のコードについてよく理解してください。

  1. アプリで sleepdetail パッケージを見つけます。このパッケージには、1 晩の睡眠の詳細を表示するフラグメントのフラグメント、ビューモデル、ビューモデル ファクトリが含まれています。

  2. sleepdetail パッケージで、SleepDetailViewModel のコードを開いて調べます。このビューモデルは、コンストラクタで SleepNight のキーと DAO を受け取ります。

    クラスの本体には、指定されたキーの SleepNight を取得するコードと、[閉じる] ボタンが押されたときに SleepTrackerFragment に戻るナビゲーションを制御する navigateToSleepTracker 変数があります。

    getNightWithId() 関数は LiveData<SleepNight> を返し、SleepDatabaseDaodatabase パッケージ内)で定義されています。

  3. sleepdetail パッケージで、SleepDetailFragment のコードを開いて調べます。データ バインディング、ビューモデル、ナビゲーションのオブザーバーの設定に注目してください。

  4. sleepdetail パッケージで、SleepDetailViewModelFactory のコードを開いて調べます。

  5. レイアウト フォルダで fragment_sleep_detail.xml を調べます。<data> タグで定義された sleepDetailViewModel 変数に注目してください。この変数は、各ビューに表示するデータをビューモデルから取得します。

    レイアウトには、睡眠の質を表す ImageView、質の評価を表す TextView、睡眠時間を表す TextView、詳細フラグメントを閉じる Button を含む ConstraintLayout が含まれています。

  6. 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: クリック リスナーを作成し、アイテム レイアウトからトリガーする

  1. sleeptracker フォルダで、SleepNightAdapter.kt を開きます。
  2. ファイルの末尾の最上位レベルで、新しいリスナー クラス SleepNightListener を作成します。
class SleepNightListener() {
    
}
  1. SleepNightListener クラス内に onClick() 関数を追加します。リスト項目を表示するビューがクリックされると、ビューはこの onClick() 関数を呼び出します。(ビューの android:onClick プロパティは、後でこの関数に設定します)。
class SleepNightListener() {
    fun onClick() = 
}
  1. SleepNight 型の関数引数 nightonClick() に追加します。ビューは表示しているアイテムを認識しており、クリックを処理するためにその情報を渡す必要があります。
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. onClick() の動作を定義するには、SleepNightListener のコンストラクタで clickListener コールバックを提供し、onClick() に割り当てます。

    クリックを処理するラムダに clickListener という名前を付けると、クラス間で渡されるときに追跡しやすくなります。clickListener コールバックは、データベースからデータにアクセスするために night.nightId のみを必要とします。完成した SleepNightListener クラスは次のようになります。
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. list_item_sleep_night.xml. を開きます。
  2. data ブロック内で、データ バインディングを通じて SleepNightListener クラスを使用できるようにする新しい変数を追加します。新しい <variable>clickListener.name を指定します。次に示すように、type をクラス com.example.android.trackmysleepquality.sleeptracker.SleepNightListener の完全修飾名に設定します。このレイアウトから SleepNightListeneronClick() 関数にアクセスできるようになりました。
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. このリストアイテムの任意の場所のクリックをリッスンするには、ConstraintLayoutandroid:onClick 属性を追加します。

    次の例に示すように、データ バインディング ラムダを使用して属性を clickListener:onClick(sleep) に設定します。
android:onClick="@{() -> clickListener.onClick(sleep)}"

ステップ 2: クリック リスナーをビューホルダーとバインディング オブジェクトに渡す

  1. SleepNightAdapter.kt を開きます。
  2. val clickListener: SleepNightListener を受け取るように SleepNightAdapter クラスのコンストラクタを変更します。アダプターが ViewHolder をバインドするときに、このクリック リスナーを提供する必要があります。
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. onBindViewHolder() で、holder.bind() の呼び出しを更新して、クリック リスナーも ViewHolder に渡します。関数呼び出しにパラメータを追加したため、コンパイラ エラーが発生します。
holder.bind(getItem(position)!!, clickListener)
  1. bind()clickListener パラメータを追加します。これを行うには、下のスクリーンショットに示すように、エラーにカーソルを置き、エラーで Alt+Enter(Windows)または Option+Enter(Mac)を押します。

  1. ViewHolder クラスの bind() 関数内で、クリック リスナーを binding オブジェクトに割り当てます。バインディング オブジェクトを更新する必要があるため、エラーが表示されます。
binding.clickListener = clickListener
  1. データバインディングを更新するには、プロジェクトをクリーンして再ビルドします。(キャッシュの無効化も必要になる場合があります)。アダプターのコンストラクタからクリック リスナーを取得し、ビューホルダーとバインディング オブジェクトに渡しました。

ステップ 3: 項目がタップされたときにトーストを表示する

クリックをキャプチャするコードは用意できましたが、リストアイテムがタップされたときに何が起こるかは実装していません。最も簡単なレスポンスは、アイテムがクリックされたときに nightId を示すトーストを表示することです。これにより、リスト項目がクリックされたときに正しい nightId がキャプチャされて渡されることが検証されます。

  1. SleepTrackerFragment.kt を開きます。
  2. onCreateView()adapter 変数を見つけます。クリック リスナー パラメータが想定されるようになったため、エラーが表示されます。
  3. SleepNightAdapter にラムダを渡して、クリック リスナーを定義します。この単純なラムダは、以下に示すように、nightId を示すトーストを表示するだけです。Toast をインポートする必要があります。更新された定義の全文は次のとおりです。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. アプリを実行し、アイテムをタップして、正しい nightId を含むトーストが表示されることを確認します。アイテムの nightId 値は増加しており、アプリは最新の夜を最初に表示するため、nightId の値が最も低いアイテムがリストの一番下に表示されます。

このタスクでは、RecyclerView のアイテムがクリックされたときの動作を変更して、トーストを表示する代わりに、クリックされた夜の詳細情報を表示する詳細フラグメントにアプリが移動するようにします。

ステップ 1: クリックで移動する

このステップでは、トーストを表示するだけでなく、SleepTrackerFragmentonCreateView() のクリック リスナー ラムダを変更して、nightIdSleepTrackerViewModel に渡し、SleepDetailFragment へのナビゲーションをトリガーします。

クリック ハンドラ関数を定義します。

  1. SleepTrackerViewModel.kt を開きます。
  2. SleepTrackerViewModel の最後の方で、onSleepNightClicked() クリック ハンドラ関数を定義します。
fun onSleepNightClicked(id: Long) {

}
  1. onSleepNightClicked() の中で、クリックされた睡眠日の id_navigateToSleepDetail に設定してナビゲーションをトリガーします。
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. _navigateToSleepDetail を実装します。これまでと同様に、ナビゲーション状態の private MutableLiveData を定義します。また、それに対応する公開 gettable val もあります。
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. アプリのナビゲーションが完了した後に呼び出すメソッドを定義します。onSleepDetailNavigated() という名前にして、値を null に設定します。
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

クリック ハンドラを呼び出すコードを追加します。

  1. SleepTrackerFragment.kt を開き、アダプターを作成して SleepNightListener を定義し、トーストを表示するコードまでスクロールします。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. 次のコードをトーストの下に追加して、項目がタップされたときに sleepTrackerViewModel のクリック ハンドラ onSleepNighClicked() を呼び出します。nightId を渡して、ビューモデルがどの睡眠夜を取得するかを把握できるようにします。onSleepNightClicked() はまだ定義されていないため、エラーは表示されたままです。トーストは、必要に応じて保持、コメントアウト、削除できます。
sleepTrackerViewModel.onSleepNightClicked(nightId)

クリックを監視するコードを追加します。

  1. SleepTrackerFragment.kt を開きます。
  2. 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()
            }
        })
  1. コードを実行し、アイテムをクリックすると、アプリがクラッシュします。

バインディング アダプタで null 値を処理します。

  1. デバッグモードでアプリを再度実行します。項目をタップし、ログをフィルタしてエラーを表示します。次のような内容を含むスタック トレースが表示されます。
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

残念ながら、スタック トレースではこのエラーがトリガーされた場所が明確に示されていません。データ バインディングの欠点の 1 つは、コードのデバッグが難しくなることです。項目をクリックするとアプリがクラッシュし、新しいコードはクリック処理のみです。

ただし、この新しいクリック処理メカニズムにより、バインディング アダプターが itemnull 値で呼び出されるようになりました。特に、アプリが起動すると LiveDatanull として起動するため、各アダプタに null チェックを追加する必要があります。

  1. BindingUtils.kt で、各バインディング アダプタについて、item 引数の型を null 値許容に変更し、本文を item?.let{...} でラップします。たとえば、sleepQualityString のアダプターは次のようになります。他のアダプタも同様に変更します。
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. アプリを実行します。項目をタップすると、詳細ビューが開きます。

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 のレイアウト ファイルに追加する。

次のレッスンに進む: 7.5: RecyclerView のヘッダー