虎視眈々と

Flutter × Firebaseを研究するアプリエンジニア

Google I/O 2018アプリで使われているAdapterのクリックイベントをViewModelで受け取り、LiveDataを使ってFragment/Activityに通知する

Google I/O 2018アプリで使われているAdapterのクリックイベントをViewModelで受け取り、LiveDataを使ってFragment/Activityに通知する

Google I/O 2018 のセッションスケジュールなどがみれるアプリがかなり勉強になるので、ソースコードを読んでて学んだことを一つ書いてみようと思います。

アプリのダウンロードはこちら

https://play.google.com/store/apps/details?id=com.google.samples.apps.iosched

ソースコードはこちら

https://github.com/google/iosched

AdapterのクリックイベントをViewModelで受け取り、LiveDataを使ってFragment/Activityに通知する

MVVMアーキテクチャとして基本的なロジックはViewModelに書きます。 ですが、画面遷移などViewに関わる処理はFragment or Activityでないと画面遷移できません。

ViewModelからViewへの処理はDatabinding経由でやることになっているので直接ViewModelからViewの処理を呼び出すことはできません。 なので、GoogleI/OアプリはLiveDataを使ってその辺りを解決しています。

例えばスケジュールを詳細を開く時のタップイベントはViewModelで受け取るようにして、ViewModelでLiveDataを使って変更をFragmentに検知させて画面遷移をしています。

スケジュールアイテムをタップする

ViewModelのメンバ変数にLiveDataのメンバ変数を設定

実際のソースコードに載せていきます。

ViewModelのメンバ変数にLiveDataのメンバ変数を設定しています。

  • ScheduleViewModel.kt
private val _navigateToSessionAction = MutableLiveData<Event<String>>()
val navigateToSessionAction: LiveData<Event<String>>
   get() = _navigateToSessionAction

FragmentでLiveDataの変更をObserveする

FragmentでLiveDataの変更をobserveする

  • ScheduleFragment.kt
scheduleViewModel.navigateToSessionAction.observe(this, EventObserver { sessionId ->
       openSessionDetail(sessionId)
})

この設定をした時点で ScheduleViewModel の中で navigateToSessionActionsetValue された時点でFragmentに通知されて openSessionDetail(sessionId) メソッドが呼び出されることになります。 ( openSessionDetail メソッドでは中で startActivity が実行されています。

RecyclerViewAdapterでのアイテムのタップイベントを検知してViewModelで受け取る

ViewModelの中で setValue をどういう風に呼び出すかというとこれもDatabindingを使ってうまくアーキテクチャにのせています。

ScheduleViewModel クラスは ScheduleEventListener というinterfaceを実装しています。

この実装の中の openEventDetail メソッドで setValue() が実行されています。

override fun openEventDetail(id: SessionId) {
   _navigateToSessionAction.value = Event(id)
}

RecylerViewAdapterにアイテムのタップイベントを設定する

セッションのアイテムは ScheduleDayFragment.kt クラスにてRecyclerViewを生成する処理が実装されています。 正確にいうと ScheduleFragment.ktScheduleDayFragment を生成しています。

これは上タブを実装するために、ViewPagerを使っているからです。

ScheduleDayFragment.kt をみていきます。

ScheduleDayAdapter にviewModelをコンストラクタとして渡していますが、実際には ScheduleEventListener の実装を渡しています。

  • ScheduleDayFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    adapter = ScheduleDayAdapter(
         viewModel,
         tagViewPool,
         viewModel.showReservations,
         viewModel.timeZoneId,
         this)
}

ScheduleDayAdapterの中でeventListenerをbindする

  • item_session.xml
<data>
   <variable
       name="eventListener"
       type="com.google.samples.apps.iosched.ui.sessioncommon.EventActions" />
</data>

・・・・・・・・もろもろのレイアウトの実装・・・・・・・・・・

<!--ここにタップイベントが仕込まれている↓-->
<androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?selectableItemBackground"
        android:onClick="@{() -> eventListener.openEventDetail(userSession.session.id)}"
        android:paddingEnd="@dimen/spacing_normal"
        android:paddingVertical="@dimen/spacing_normal"
        tools:targetApi="o">

xml内の android:onClick を使ってボタンのタップイベントを仕込んでいます。

この仕込まれたタップイベントをRecyclerViewAdapterのViewHolderで引数で渡ってきた ScheduleEventListener をbindしています。

fun bind(userSession: UserSession) {
   binding.userSession = userSession
   // ↓ここで引数をバインドしている
   binding.eventListener = eventListener
   binding.showReservations = showReservations
   binding.timeZoneId = timeZoneId
   binding.setLifecycleOwner(lifecycleOwner)
   binding.executePendingBindings()
}

(このコードなぜか apply を使ってかかれていないのは謎です........)

これで全てが連動されてタップイベントがFragmentで実行されていることになります。

まとめ

  • ViewModelにLiveDataのメンバ変数を設定
  • FragmentでLiveDataの変数が setValue されるのをobserveする
  • ViewModelの側でボタンのタップイベントを検知するinterfaceを実装している
  • ViewModelの実装をRecylerViewAdapterに引数として渡す
  • RecylerViewAdapterで生成する子のViewに引数で渡ってきたリスナーをDatabindingを使ってセットしてクリックイベントを発火させる

これから

次はDaggaar2でDIしている箇所をちゃんと理解したい。 ここを理解できたら「Google I/Oアプリ完全に理解したわー」になれる気がする。