Android Kotlin の基礎 05.3: ViewModel と LiveData を使用したデータ バインディング

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

はじめに

このレッスンの前の Codelab では、GuessTheWord アプリのコードを改善しました。アプリは ViewModel オブジェクトを使用するようになったため、画面の回転やキーボードの利用可否の変更などのデバイス設定の変更後もアプリデータが引き継がれます。また、監視可能な LiveData を追加したため、監視対象のデータが変更されるとビューに自動的に通知されます。

この Codelab では、GuessTheWord アプリを引き続き使用します。レイアウト内のビューが ViewModel オブジェクトと直接通信できるように、アプリ内の ViewModel クラスにビューをバインドします。(これまでのアプリでは、ビューはアプリのフラグメントを介して ViewModel間接的に通信していました)。データ バインディングを ViewModel オブジェクトと統合したら、アプリのフラグメントでクリック ハンドラは不要になるため、削除します。

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

前提となる知識

  • Kotlin で基本的な Android アプリを作成する方法。
  • アクティビティとフラグメントのライフサイクルの仕組み。
  • アプリで ViewModel オブジェクトを使用する方法。
  • ViewModelLiveData を使用してデータを保存する方法。
  • LiveData データの変更を監視するオブザーバー メソッドを追加する方法。

学習内容

  • データ バインディング ライブラリの要素の使用方法。
  • ViewModel をデータ バインディングと統合する方法。
  • LiveData をデータ バインディングと統合する方法。
  • リスナー バインディングを使用して、フラグメント内のクリック リスナーを置き換える方法。
  • データ バインディング式に文字列の書式設定を追加する方法。

演習内容

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

レッスン 5 の Codelab では、スターター コードから GuessTheWord アプリを開発します。GuessTheWord は、2 人のプレーヤーが最高スコアを目指して協力する、2 プレーヤー ジェスチャー ゲームです。

最初のプレーヤーはアプリの単語を見て、2 番目のプレーヤーに単語を見せないようにしながら、各単語を順番に演じます。2 人目のプレーヤーが単語を当てようとします。

ゲームをプレイするには、最初のプレーヤーがデバイスでアプリを開き、下のスクリーンショットに示すように、「ギター」などの単語を表示します。

最初のプレーヤーは、単語自体を言わないように注意しながら、単語を演じます。

  • 2 人目のプレーヤーが単語を正しく当てると、1 人目のプレーヤーが [Got It] ボタンを押します。これにより、カウントが 1 つ増え、次の単語が表示されます。
  • 2 人目のプレーヤーが単語を当てられない場合、1 人目のプレーヤーが [スキップ] ボタンを押します。これにより、カウントが 1 減り、次の単語にスキップします。
  • ゲームを終了するには、[End Game] ボタンを押します。(この機能は、シリーズの最初の Codelab のスターター コードには含まれていません)。

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

タイトル画面

ゲーム画面

スコア画面

このタスクでは、この Codelab のスターター コードを見つけて実行します。前の Codelab で作成した GuessTheWord アプリをスターター コードとして使用することも、スターター アプリをダウンロードすることもできます。

  1. (省略可)前の Codelab のコードを使用しない場合は、この Codelab のスターター コードをダウンロードします。コードの ZIP ファイルを展開し、プロジェクトを Android Studio で開きます。
  2. アプリを実行して、ゲームをプレイします。
  3. [Got It] ボタンをタップすると、次の単語が表示され、スコアが 1 増えます。[Skip] ボタンをタップすると、次の単語が表示され、スコアが 1 減ります。[ゲームを終了] ボタンをクリックすると、ゲームが終了します。
  4. すべての単語を切り替えると、アプリが自動的にスコア画面に移動します。

前の Codelab では、GuessTheWord アプリのビューにアクセスする型安全な方法としてデータ バインディングを使用しました。しかし、データ バインディングの真の力は、その名前が示すように、アプリのビュー オブジェクトにデータを直接バインドすることにあります。

現在のアプリのアーキテクチャ

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

次に例を示します。

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

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

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

レイアウト内のビューが UI コントローラを仲介として使用せずに、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 プラグイン バージョン 2.0 以降で機能します。詳しくは、レイアウトとバインディング式をご覧ください。

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

  1. game_fragment.xml で、skip_buttononClick 属性を追加します。バインディング式を定義し、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()
}

ステップ 3: 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 で、play_again_buttononClick 属性を追加します。リスナー バインディングを定義し、ScoreViewModelonPlayAgain() メソッドを呼び出します。
<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. アプリを実行します。アプリは以前と同じように動作しますが、ボタンビューは 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

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

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

ステップ 1: game_fragment.xml ファイルに単語 LiveData を追加する

このステップでは、現在の単語のテキストビューを ViewModelLiveData オブジェクトに直接バインドします。

  1. game_fragment.xml で、word_text テキストビューに android:text 属性を追加します。

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

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

word.value を使用する必要はありません。代わりに、実際の LiveData オブジェクトを使用できます。LiveData オブジェクトは、word の現在の値を表示します。word の値が null の場合、LiveData オブジェクトには空の文字列が表示されます。

  1. GameFragment または onCreateView() で、gameViewModel を初期化した後、現在のアクティビティを binding 変数のライフサイクル所有者として設定します。これにより、上記の LiveData オブジェクトのスコープが定義され、オブジェクトがレイアウト内のビュー game_fragment.xml を自動的に更新できるようになります。
binding.gameViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. GameFragment で、LiveData word のオブザーバーを削除します。

削除するコード:

/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})
  1. アプリを実行して、ゲームをプレイします。これで、UI コントローラのオブザーバー メソッドなしで現在の単語が更新されるようになりました。

ステップ 2: score_fragment.xml ファイルにスコア LiveData を追加する

このステップでは、LiveData score をスコア フラグメントのスコア テキストビューにバインドします。

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

削除するコード:

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

ステップ 3: データ バインディングで文字列の書式設定を追加する

レイアウトでは、データ バインディングとともに文字列の書式設定を追加できます。このタスクでは、現在の単語をフォーマットして、その単語を引用符で囲みます。また、次の図に示すように、スコア文字列の先頭に「Current Score」という接頭辞を付加するようにフォーマットします。

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

削除するコード:

viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. アプリをクリーン、再ビルド、実行してから、ゲームをプレイします。現在の単語とスコアがゲーム画面でフォーマットされています。

これで、アプリで LiveDataViewModel をデータ バインディングと統合しました。これにより、レイアウト内のビューは、フラグメントのクリック ハンドラを使用することなく、ViewModel と直接通信できます。また、LiveData オブザーバー メソッドを使用せずに、LiveData オブジェクトをデータ バインディング ソースとして使用して、データの変更について UI に自動的に通知しました。

Android Studio プロジェクト: GuessTheWord

  • データ バインディング ライブラリは、ViewModelLiveData などの Android アーキテクチャ コンポーネントとシームレスに連携します。
  • アプリのレイアウトはアーキテクチャ コンポーネントのデータにバインドできます。この機能は、UI コントローラのライフサイクルを管理し、データの変更について通知するのに役立っています。

ViewModel のデータ バインディング

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

ViewModel をレイアウトに関連付ける方法は次のとおりです。

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

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  • GameFragment ファイルで、GameViewModel をデータ バインディングに渡します。
binding.gameViewModel = viewModel

リスナー バインディング

  • リスナー バインディングは、onClick() などのクリック イベントがトリガーされたときに実行されるレイアウト内のバインディング式です。
  • リスナー バインディングはラムダ式として記述される。
  • リスナー バインディングを使用して、UI コントローラのクリック リスナーをレイアウト ファイルのリスナー バインディングに置き換えます。
  • データ バインディングではリスナーを作成してビューに設定します。
 android:onClick="@{() -> gameViewModel.onSkip()}"

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

  • LiveData オブジェクトをデータ バインディング ソースとして使用することで、データの変更について UI に自動的に通知できます。
  • ビューを ViewModelLiveData オブジェクトに直接バインドできます。ViewModelLiveData が変更されると、UI コントローラのオブザーバー メソッドを使用しなくても、レイアウト内のビューを自動的に更新できます。
android:text="@{gameViewModel.word}"
  • LiveData データ バインディングを機能させるには、現在の Activity(UI コントローラ)を UI コントローラの binding 変数のライフサイクル オーナーとして設定します。
binding.lifecycleOwner = this

データ バインディングによる文字列の書式設定

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

Udacity コース:

Android デベロッパー ドキュメント:

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

以下の質問に回答してください

問題 1

リスナー バインディングの説明として正しくないものは次のうちどれですか。

  • リスナー バインディングは、イベントの発生時に実行されるバインディング式である。
  • リスナー バインディングは、Android Gradle プラグインのすべてのバージョンで機能する。
  • リスナー バインディングはラムダ式として記述される。
  • リスナー バインディングはメソッド参照に似ているが、任意のデータ バインディング式を実行できる。

問題 2

アプリに文字列リソース
<string name="generic_name">Hello %s</string> が含まれているとします。

データ バインディング式を使用して文字列をフォーマットする構文として、正しいものは次のうちどれですか。

  • android:text= "@{@string/generic_name(user.name)}"
  • android:text= "@{string/generic_name(user.name)}"
  • android:text= "@{@generic_name(user.name)}"
  • android:text= "@{@string/generic_name,user.name}"

問題 3

リスナー バインディング式は、どのようなときに評価され実行されますか。

  • LiveData で保持されているデータが変更されたとき
  • 設定の変更によってアクティビティが再作成されたとき
  • onClick() などのイベントが発生したとき
  • アクティビティがバックグラウンドに移動したとき

次のレッスンに進む: 5.4: LiveData 変換

このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。