Android Kotlin基礎講座 05.3: ViewModelとLiveDataのデータバインディング

タスク:ViewModelデータバインディングを追加する

前回の記事ではGuessTheWordアプリ内のビューに接続する際に型安全な方法としてデータバインディングを利用しました。しかしデータバインディングの本領はその名前が示しているように、アプリ内のビューに直接データを結合することで発揮されます。

現在のアプリの構造

開発中のアプリでは、ビューはXMLレイアウト内に定義されており、それらのビュー用のデータはViewModelオブジェクトに保存されています。それぞれのビューとそれに対応するViewModel間にはUI controllerがあり、それらを中継しています。

例:

  • Got itボタンはButtonビューとしてgame_fragment.xmlというレイアウトファイルに定義されています。
  • ユーザーがGot itボタンをタップすると、GameFragmentフラグメント内のクリックリスナーがGameViewModel内の対応するクリックリスナーを呼び出します。
  • GameViewModel内の得点が更新されます。

ButtonビューとGameViewModelは直接やり取りはしていません。GameFragment内にあるクリックリスナーが必要になります。

データバインディングにViewModelが渡されると

仲介役としてのUI controller無しに、レイアウトのビューがViewModelオブジェクト内のデータと直接やり取りすれば、より簡素化することができます。

ViewModelオブジェクトはGuessTheWordアプリ内の全てのUIデータを保持します。ViewModelオブジェクトをデータバインディングに渡すことで、ビューとViewModelオブジェクト間のやり取りを自動化することができます。

このタスクでは、GameViewModelとScoreViewModelクラスをそれらに対応するXMLレイアウトと紐づけます。またクリックイベントを扱うためのリスナーバインディングをセットアップしていきます。

ステップ1:GameViewModel用のデータバインディングを追加する

このステップでは、GameViewModelとそれに対応するレイアウトファイルであるgame_fragment.xmlを紐づけます。

  1. game_fragment.xmlファイル内にGameViewModel型のデータバインディング変数を追加してください。Android Studioにエラーが表示された場合、クリーンアップ後、リビルドしてください。
<layout ...>

   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  
   <androidx.constraintlayout...
  1. GameFragmentファイル内で、GameViewModelをデータバインディングに渡してください。

    これをするためには、binding.gameViewModel変数に上のステップで宣言したviewModelを代入します。このコードをonCreateView()のviewModelの初期化文の後に追加してください。Android Studioにエラーが表示された場合、クリーンアップ後、リビルドしてください。
// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel
binding.gameViewModel = viewModel

ステップ2:イベントを扱うためのリスナーバインディングを使う

リスナーバインディングはonClick()、onZoomIn()やonZoomOut()のようなイベントが引き起こされた際に実行されるバインディング式です。リスナーバインディングはラムダ式で書かれます。

データバインディングはリスナーを生成し、ビューにそのリスナーをセットします。イベントが発生すると、リスナーはラムダ式を計算します。リスナーバインディングはAndroid Gradle Pluginバージョン2.0かそれ以降で動作します。

このステップではGameFragment内のクリックリスナーをgame_fragment.xmlファイル内のリスナーバインディングに置き換えていきます。

  1. game_fragment.xmlで、skip_buttonにonClick属性を追加してください。バインディング式を定義し、GameViewModelのonSkip()メソッドを呼び出してください。このバインディング式がリスナーバインディングと呼ばれています。
<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />
  1. 同様に、correct_buttonのクリックイベントをGameViewModelのonCorrect()メソッドに結合してください。
<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onCorrect()}"
   ... />
  1. end_game_buttonのクリックイベントをGameViewModelのonGameFinish()メソッドに結合してください。
<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />
  1. GameFragment内のクリックリスナーをセットしている文とクリックリスナーが呼び出している関数を削除してください。それらはもう必要ありません。

削除するコード:

binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
binding.endGameButton.setOnClickListener { onEndGame() }

/** Methods for buttons presses **/
private fun onSkip() {
   viewModel.onSkip()
}
private fun onCorrect() {
   viewModel.onCorrect()
}
private fun onEndGame() {
   gameFinished()
}

ステップ3:ScoreViewModel用のデータバインディングを追加する

このステップでは、ScoreViewModelとそれに対応するレイアウトファイルであるscore_fragment.xmlを紐づけます。

  1. score_fragment.xmlファイルにScoreViewModel型のバインディング変数w追加してください。このステップは上でGameViewModelに対して行ったこととほぼ同じです。
<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
  1. score_fragment.xml内のplay_again_buttonにonClick属性を追加してください。
    リスナーバインディングを低gし、ScoreViewModelのonPlayAgain()メソッドを呼び出してください。
<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />
  1. ScoreFragmentのonCreateView()内のviewModelを初期化文のあとで、binding.scoreViewModelというバインディング変数を初期化してください。
viewModel = ...
binding.scoreViewModel = viewModel
  1. ScoreFragmento内のPlayAgainButtonにクリックリスナーをセットしているコードを削除してください。Android Studioにエラーが表示された場合、クリーンアップしてリビルドしてください。

削除するコード:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. アプリを起動してください。アプリは以前と同じように動作するはずですが、現在はボタンビューはViewModelオブジェクトと直接やり取りしています。もうビューはScoreFragmentのクリックハンドラーを経由してやり取りすることはありません。

データバインディングエラー文のトラブルシューティング

アプリがデータバインディングを使用する際、コンパイル処理によってデータバインディングのために使われる中間クラスが生成されます。それによりアプリをコンパイルしようとするまでAndroid Studioが検知できないエラーがアプリに含まれている場合があり、コードを書いている際には警告や赤文字が表示されません。しかしコンパイル時に生成された中間クラスから発生する一見不可解なエラーに遭遇する場合あります。

エラーメッセージが表示された場合:

  1. Android StudioのBuildパネル中のメッセージをよく読んでください。databindingで終わる箇所があった場合、データバインディングに関連するエラーがあります。
  2. XMLレイアウトファイル中の、データバインディングを使っているonClick属性に含まれるエラーを確認してください。
    ラムダ式が呼び出している関数を探し、それが存在していることを確認してください。
  3. XMLの<data>セクションの中で、データバインディング変数のスペルを確認してください。

例として、以下の属性に含まれる関数名onCorrect()のミススペルなどがあります。

android:onClick="@{() -> gameViewModel.onCorrectx()}"

またXMLファイルの<data>セクション中のgameViewModelのミススペルなどもありえます。

<data>
   <variable
       name="gameViewModelx"
       type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>

Android Studioがアプリをコンパイルするまでこれらのエラーは検知できません。コンパイル時には以下のようなエラー文が表示されます。

error: cannot find symbol
import com.example.android.guesstheword.databinding.GameFragmentBindingImpl"

symbol:   class GameFragmentBindingImpl
location: package com.example.android.guesstheword.databinding