Android Kotlin基礎講座 07.4:RecyclerViewのタップ処理

タスク:アイテムをクリックできるようにする

このタスクでは、RecyclerViewをアップデートして、ユーザーのタップに反応してタップされたアイテムの詳細を画面に表示できるようにします。

クリックを受け取り、それを処理するというのは二つのタスクに分けて考えることができます。一つはクリックを受け取り、どのアイテムがクリックされたのかを決めるタスクです。それからそのクリックに対する反応処理をタスクです。

とすると、このアプリにおいてクリックリスナーを追加すべき場所はどこになるでしょうか。

  • SleepTrackerFragmentはたくさんのビューをホストしているので、クリックイベントをこのフラグメントで受け取ると、どのアイテムがクリックされたのかを知ることができません。また、クリックされたのがアイテムなのか、はたまたUI要素なのかを知ることもできません。
  • RecyclerViewのレベルでクリックイベントを受け取ると、リスト内のどのアイテムが実際にクリックされたのかを知るのが困難です。
  • クリックされたアイテムに関する情報を得るための最もいい場所はViewHolderオブジェクト内です。なぜならViewHolderは一つのリストアイテムを表すオブジェクトだからです。

ViewHolderはクリックを受け取る場所として適切ではありますが、それらを処理する場所としては通常ふさわしくありません。とするとクリックを処理する場所として適切なのはどこでしょうか。

  • Adapterはビュー内のデータアイテムを表示します。ですので、アダプター内でクリックを処理することもできます。しかし、アダプターの役割はデータを表示するために適合させることであり、アプリのロジックを扱うことではありません。
  • 普通はViewModel内でクリックを処理します。なぜならViewModelはデータとクリックに対する反応として何を起こすべきかを決定するロジックへのアクセスを持っているからです。

Tip: RecyclerViewにクリックリスナーを実装するための他のパターンもありますが、この記事で行うパターンは説明しやすく、実装が最も簡単です。Androidアプリを開発していく際には、RecyclerViewでクリックリスナーを使う別のパターンにも遭遇するでしょう。全てのパターンには各々の利点があります。

ステップ1:クリックリスナーを作成し、アイテムレイアウトからトリガーする

  1. sleeptrackerフォルダー内のSleepNightAdapter.ktを開いてください。
  2. ファイルの一番下、トップレベルに新規リスナークラスである、SleepNightListenerを作成してください。
class SleepNightListener() {
    
}
  1. SleepNightListenerクラスの中に、onClick()関数を追加してください。リストアイテムを表示するビューがクリックされると、そのビューはこのonClick()関数を呼び出します。(この関数の後にビューのプロパティであるandroid:onClickを設定します)
class SleepNightListener() {
    fun onClick() = 
}
  1. onClick()の引数としてSleepNight型のnight引数を追加してください。ビューは自身がどのアイテムを表示しているのかを知っており、その情報がクリック処理を行うために渡される必要があります。
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ブロックの中に、データバインディングを通してSleepNightListenrクラスを使えるようにするための新しい変数を追加します。
    新規<variable>にclickListenerという名前を付与してください。typeにはクラスの完全修飾名である、com.example.android.trackmysleepquality.sleeptracker.SleepNightListenerを設定してください。
    これでこのレイアウトからSleepNightListenrのonClick()関数にアクセスできるようになりました。
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. このリストアイテムのどれからでもクリックを受け付けられるようにするために、ConstraintLayoutにandroid:onClickを追加してください。

以下のようにデータバインディングラムダ式を使って属性を設定してください。

android:onClick="@{() -> clickListener.onClick(sleep)}"

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

  1. SleepNightAdapter.ktを開いてください。
  2. SleepNightAdapterクラスのコンストラクタを修正して、val clickListener: SleepNightListenerを受け取るようにしてください。アダプターが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(Macの場合Option+Enter)を押してください。
  1. ViewHolderクラス内、bind()関数内で、クリックリスナーをbindingオブジェクトに代入してください。バインディングオブジェクトを更新する必要があるため、エラーが発生します。
binding.clickListener = clickListener
  1. データバインディングを更新するために、プロジェクトをCleanしてリビルドしてください。(キャッシュを無効にする必要がある場合もあります)
    これでアダプターのコンストラクタからクリックリスナーを受け取り、それをビューホルダーとバインディングオブジェクトに渡すようにしました。
    ※この時点ではエラーが発生しているのでアプリは起動しません。

ステップ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の値をもつアイテムはリストの一番下に表示されます。