iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day17【Data binding with ViewModel and LiveData編】

学ぶこと

すること

  • GuessTheWord レイアウトの View は、UI controller(fragment) を使用して情報を中継し、ViewModel オブジェクトと間接的に通信します。このコードラボでは、アプリの View を ViewModel オブジェクトにバインドして、View が ViewModel オブジェクトと直接通信するようにします。
  • LiveData をデータバインディングソースとして使用するようにアプリを変更します。この変更後、LiveData オブジェクトはデータの変更について UI に通知し、LiveData オブザーバーメソッドは不要になります。

アプリの概要

このコードラボでは、ViewModel オブジェクトの LiveData とデータバインディングを統合することにより、GuessTheWord アプリを改善します。これにより、レイアウト内の View と ViewModel オブジェクト間の通信が自動化され、LiveData を使用してコードを簡素化できます。

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

以前のコードラボでは、GuessTheWord アプリの View にアクセスするためのタイプセーフな方法としてデータバインディングを使用していました。ただし、データバインディングの真の力は、データをアプリの View オブジェクトに直接バインドすることにあります。

現在の App Architecture

アプリでは、View は XML レイアウトで定義され、それらの View のデータは ViewModel オブジェクトに保持されます。各 View とそれに対応する ViewModel の間には、View 間のリレーとして機能する UI controller があります。

example:

  • Got It ボタンは、game_fragment.xml レイアウトファイルの Button view として定義されています。
  • ユーザーが Got It ボタンをタップすると、GameFragment のクリックリスナーが GameViewModel の対応するクリックリスナーを呼び出します。
  • スコアは GameViewModel で更新されます。

Button view と GameViewModel は直接通信しません。これらには、GameFragment にあるクリックリスナーが必要です。

data binding に渡された ViewModel

レイアウト内の View が、中間として UI controller に依存せずに、ViewModel オブジェクト内のデータと直接通信する場合はより簡単になります。

ViewModel オブジェクトは、GuessTheWord アプリの全ての UI データを保持します。ViewModel オブジェクトをデータバインディングに渡すことで、View と ViewModel オブジェクト間の通信の一部を自動化できます。

このタスクでは、GameViewModel クラスと、ScoreViewModel クラスを対応する XML レイアウトに関連付けます。また、クリックイベントを処理するリスナーバインディングを設定します。

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 を割り当てます。viewModel が初期化された後、このコードを onCreateView() 内に配置します。Android Studio でエラーが発生した場合は、プロジェクトをクリーンアップして再構築してください。

// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel
binding.gameViewModel = viewModel

イベント処理にリスナーバインディングを使用する

Listener bindings は、onClick()onZoomIn()onZoomOut() などのイベントがトリガーされた時に実行されるバインディング式です。リスナーバインディングラムダ式として記述されます。

データバインディングはリスナーを作成し、View にリスナーを設定します。リッスンされたイベントが発生すると、リスナーはラムダ式を評価します。リスナーバインディングは、Android Gradle Plugin version 2.0 以降で機能します。詳細については、Layout and binding expressions 式を参照してください。

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

  1. game_fragment.xml で、onClick attributes を skip_button に追加します。バインディング式を定義し、GameViewModelonSkip() メソッドを呼び出します。このバインディング式は、リスナーバインディングと呼ばれます。
<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />
  1. 同様に、correct_button のクリックイベントを GameViewModelonCorrect() メソッドにバインドします。
<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onCorrect()}"
   ... />
  1. end_game_button のクリックイベントを GameViewModelonGameFinish() メソッドにバインドします。
<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()
}

ScoreViewModel のデータバインディングを追加します

このステップでは、ScoreViewModel を対応するレイアウトファイル score_fragment.xml に関連付けます。

  1. score_fragment.xml ファイルに、ScoreViewModel タイプのバインディング変数を追加します。この手順は、上記の GameViewModel に対して行った手順と似ています。
<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
  1. score_fragment.xml で、onClick attribute を play_again_button に追加します。リスナーバインディングを定義し、ScoreViewModel で、onPlayAgain() メソッドを呼び出します。
<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />
  1. ScoreFragmentonCreateView() 内で、viewModel を初期化します。次に、binding.scoreViewModel バインディング変数を初期化します。
viewModel = ...
binding.scoreViewModel = viewModel
  1. ScoreFragment で、playAgainButton のクリックリスナーを設定するコードを削除します。Android Studio にエラーが表示された場合は、プロジェクトをクリーンアップして再構築します。

削除するコード:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. アプリを実行します。アプリは以前と同じように機能するはずですが、button view は ViewModel オブジェクトと直接通信するようになりました。View は、ScoreFragment のボタンクリックハンドラーを介して通信しなくなりました。

Troubleshooting data-binding error messages

アプリがデータバインディングを使用する場合、コンパイルプロセスは、データバインディングに使用される中間クラスを生成します。アプリには、アプリをコンパイルしようとするまで Android Studio が検出しなエラーが含まれている可能性があるため、コードの記述中に警告や赤い Error code は表示されません。ただし、コンパイル時に、生成された中間クラスから発生する不可解なエラーが発生します。

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

  1. Android StudioBuild ペインを注意深くみてください。data binding で終わる場所が表示された場合は、databinding にエラーがあります。

  2. レイアウト XML ファイルで、データバインディングを使用する onClick attributes のエラーを確認します。ラムダ式が呼び出す関数を探し、それが存在することを確認します。

  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

data binding に LiveData を追加する

データバインディングは、ViewModel オブジェクトで使用される LiveData でうまく機能します。ViewModel オブジェクトにデータバインディングを追加したので、LiveData を組み込む準備ができました。

このタスクでは、GuessTheWord アプリを変更して、LiveData をデータバインディングソースとして使用し、LiveData オブザーバーメソッドを使用せずに、データの変更について UI に通知します。

word LiveData を game_fragment.xml ファイルに追加します

このステップでは、現在の word text view を ViewModelLiveData オブジェクトに直接バインドします。

  1. game_fragment.xml で、android:text attribute を word_text text view に追加します。

バインディング変数 gameViewModel を使用して、GameViewModel からの word である LiveData オブジェクトに設定します。

<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{gameViewModel.word}"
   ... />

word.value を使用する必要がないことに注意してください。代わりに、実際の LiveData オブジェクトを使用できます。LiveData オブジェクトは、word の現在の値を表示します。word の値が null の場合、LiveData オブジェクトは空の文字列を表示します。

  1. GameFragmentonCreateView() で、gameViewModel を初期化した後、Fragment view をバインディング変数の Lifecycle Owner として設定します。これにより、上記の LiveData オブジェクトスコープが定義され、オブジェクトが game_fragment.xml の view を自動的に更新出来るようになります。
binding.gameViewModel = ...
// Specify the fragment view as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = viewLifecycleOwner
  1. GameFragment で、LiveData word のオブザーバを削除します。

削除するコード:

/** Setting up LiveData observation relationship **/
viewModel.word.observe(viewLifecycleOwner, Observer { newWord ->
   binding.wordText.text = newWord
})
  1. アプリを実行してゲームをプレイします。現在、UI controller のオブザーバーメソッドなしで現在の word が更新されています。

score LiveData を score_fragment.xml ファイルに追加する

このステップでは、LiveData score を score fragment の score text view にバインドします。

  1. score_fragment.xml で、android:text attribute を score text view に追加します。scoreViewModel.scoretext attribute に割り当てます。score は整数であるため、String.valueOf() を使用して文字列に変換します。
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{String.valueOf(scoreViewModel.score)}"
   ... />
  1. ScoreFragment で、scoreViewModel を初期化した後、現在の Activity を binding 変数の lifecycle owner として設定します。
binding.scoreViewModel = ...
// Specify the fragment view as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = viewLifecycleOwner
  1. ScoreFragment で、score オブジェクトのオブザーバーを削除します。

削除するコード:

// Add observer for score
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. アプリを実行してプレイします。score fragment にオブザーバーがなくても、score fragment のスコアが正しく表示されていることに注目してください。

Data binding を使用した文字列フォーマットの追加

レイアウトでは、Data binding とともに string formatting を追加できます。このタスクでは、現在の単語をフォーマットして、その周りに引用符を追加します。次の図に示すように、Current Score をフォーマットして、現在のスコアの前に付けます。

  1. string.xml に、次の文字列を追加します。これらの文字列を使用して、wordscore のフォーマットを行います。%s%d は、現在の word と score のプレースホルダーです。
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
  1. game_fragment.xml で、quote_format 文字列リソースを使用するように word_text テキストビューの text attribute を更新します。gameViewModel.word を渡します。これにより、現在の word が引数としてフォーマット文字列に渡されます。
<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{@string/quote_format(gameViewModel.word)}"
   ... />
  1. word_text と同様に score テキストビューをフォーマットします。game_fragment.xml で、text attribute を score_text テキストビューに追加します。%d プレースホルダーで表される1つの数値引数を取る文字列リソース score_format を使用します。この formatting string の引数として、LiveData オブジェクト score を渡します。
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{@string/score_format(gameViewModel.score)}"
   ... />
  1. GameFragment クラスの onCreateView() メソッド内で、score オブザーバーコードを削除します。

削除するコード:

viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. アプリを Clean、rebuild してからゲームをプレイします。current の word と score がゲーム画面でフォーマットされていることに注目してください。

まとめ

ViewModel データバインディング

  • data binding を使用して、ViewModel をレイアウトに関連付けることができます。
  • ViewModel オブジェクトは UI データを保持します。ViewModel オブジェクトをデータバインディングに渡すことで、View と ViewModel オブジェクト間の通信の一部を自動化できます。

ViewModel をレイアウトに関連付ける方法:

  • レイアウトファイルで、ViewModel タイプのデータバインディング変数を追加します。
   <data>

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

Listener bindings

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

Data binding への LiveData の追加

  • LiveData オブジェクトを data-binding ソースとして使用して、データの変更について UI に自動的に通知できます。
  • View を ViewModelLiveData オブジェクトに直接バインドできます。ViewModelLiveData が変更されると、UI controllers のオブザーバーメソッドなしで、レイアウトの View を自動的に更新できます。
android:text="@{gameViewModel.word}"
  • LiveData data binding を機能させるには、現在の activity(UI controller) を UI controller の binding 変数の lifecycle owner として設定します。
binding.lifecycleOwner = this

Data binding を使用した String formatting

  • データバインディングを使用すると、文字列の場合は %s、整数の場合は %d などのプレースホルダーを使用して文字列リソースをフォーマットできます。
  • View の text attribute を更新するには、LiveData オブジェクトを引数としてフォーマット文字列に渡します。
 android:text="@{@string/quote_format(gameViewModel.word)}"

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com