この 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
オブジェクトを使用する方法。 ViewModel
でLiveData
を使用してデータを保存する方法。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 アプリをスターター コードとして使用することも、スターター アプリをダウンロードすることもできます。
- (省略可)前の Codelab のコードを使用しない場合は、この Codelab のスターター コードをダウンロードします。コードの ZIP ファイルを展開し、プロジェクトを Android Studio で開きます。
- アプリを実行して、ゲームをプレイします。
- [Got It] ボタンをタップすると、次の単語が表示され、スコアが 1 増えます。[Skip] ボタンをタップすると、次の単語が表示され、スコアが 1 減ります。[ゲームを終了] ボタンをクリックすると、ゲームが終了します。
- すべての単語を切り替えると、アプリが自動的にスコア画面に移動します。
前の 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
に関連付けます。
game_fragment.xml
ファイルに、GameViewModel
型のデータ バインディング変数を追加します。Android Studio でエラーが発生した場合は、プロジェクトをクリーンして再ビルドします。
<layout ...>
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
<androidx.constraintlayout...
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
ファイルのリスナー バインディングに置き換えます。
game_fragment.xml
で、skip_button
にonClick
属性を追加します。バインディング式を定義し、GameViewModel
でonSkip()
メソッドを呼び出します。このバインディング式はリスナー バインディングと呼ばれます。
<Button
android:id="@+id/skip_button"
...
android:onClick="@{() -> gameViewModel.onSkip()}"
... />
- 同様に、
correct_button
のクリック イベントをGameViewModel
のonCorrect
()
メソッドにバインドします。
<Button
android:id="@+id/correct_button"
...
android:onClick="@{() -> gameViewModel.onCorrect()}"
... />
end_game_button
のクリック イベントをGameViewModel
のonGameFinish
()
メソッドにバインドします。
<Button
android:id="@+id/end_game_button"
...
android:onClick="@{() -> gameViewModel.onGameFinish()}"
... />
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
に関連付けます。
score_fragment.xml
ファイルに、ScoreViewModel
型のバインディング変数を追加します。このステップは、上記のGameViewModel
のステップと似ています。
<layout ...>
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
score_fragment.xml
で、play_again_button
にonClick
属性を追加します。リスナー バインディングを定義し、ScoreViewModel
でonPlayAgain()
メソッドを呼び出します。
<Button
android:id="@+id/play_again_button"
...
android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
... />
ScoreFragment
のonCreateView()
内で、viewModel
を初期化します。次に、binding.scoreViewModel
バインディング変数を初期化します。
viewModel = ...
binding.scoreViewModel = viewModel
ScoreFragment
で、playAgainButton
のクリック リスナーを設定するコードを削除します。Android Studio にエラーが表示された場合は、プロジェクトをクリーンアップして再ビルドします。
削除するコード:
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }
- アプリを実行します。アプリは以前と同じように動作しますが、ボタンビューは
ViewModel
オブジェクトと直接通信するようになります。ビューはScoreFragment
のボタン クリック ハンドラを介して通信しなくなりました。
データ バインディングのエラー メッセージのトラブルシューティング
アプリでデータ バインディングを使用すると、コンパイル プロセスでデータ バインディングに使用される中間クラスが生成されます。アプリには、アプリをコンパイルしようとするまで Android Studio が検出しないエラーが含まれている場合があります。そのため、コードの記述中に警告や赤いコードが表示されないことがあります。しかし、コンパイル時に、生成された中間クラスから不可解なエラーが発生します。
わかりにくいエラー メッセージが表示された場合:
- Android Studio の [Build] ペインに表示されるメッセージをよく確認してください。
databinding
で終わるロケーションが表示された場合は、データ バインディングにエラーがあります。 - レイアウト XML ファイルで、データ バインディングを使用する
onClick
属性のエラーを確認します。ラムダ式が呼び出す関数を探し、その関数が存在することを確認します。 - 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 を追加する
このステップでは、現在の単語のテキストビューを ViewModel
の LiveData
オブジェクトに直接バインドします。
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
オブジェクトには空の文字列が表示されます。
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
GameFragment
で、LiveData
word
のオブザーバーを削除します。
削除するコード:
/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
binding.wordText.text = newWord
})
- アプリを実行して、ゲームをプレイします。これで、UI コントローラのオブザーバー メソッドなしで現在の単語が更新されるようになりました。
ステップ 2: score_fragment.xml ファイルにスコア LiveData を追加する
このステップでは、LiveData
score
をスコア フラグメントのスコア テキストビューにバインドします。
score_fragment.xml
で、スコア テキストビューにandroid:text
属性を追加します。scoreViewModel.score
をtext
属性に割り当てます。score
は整数であるため、String.valueOf()
を使用して文字列に変換します。
<TextView
android:id="@+id/score_text"
...
android:text="@{String.valueOf(scoreViewModel.score)}"
... />
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
ScoreFragment
で、score
オブジェクトのオブザーバーを削除します。
削除するコード:
// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- アプリを実行して、ゲームをプレイします。スコア フラグメントにオブザーバーがなくても、スコア フラグメントのスコアが正しく表示されていることに注目してください。
ステップ 3: データ バインディングで文字列の書式設定を追加する
レイアウトでは、データ バインディングとともに文字列の書式設定を追加できます。このタスクでは、現在の単語をフォーマットして、その単語を引用符で囲みます。また、次の図に示すように、スコア文字列の先頭に「Current Score」という接頭辞を付加するようにフォーマットします。
string.xml
で、word
テキストビューとscore
テキストビューの書式設定に使用する次の文字列を追加します。%s
と%d
は、現在の単語と現在のスコアのプレースホルダです。
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
game_fragment.xml
で、word_text
テキストビューのtext
属性を更新して、quote_format
文字列リソースを使用するようにします。gameViewModel.word
を渡します。これにより、現在の単語が書式設定文字列の引数として渡されます。
<TextView
android:id="@+id/word_text"
...
android:text="@{@string/quote_format(gameViewModel.word)}"
... />
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)}"
... />
GameFragment
クラスのonCreateView()
メソッド内で、score
オブザーバー コードを削除します。
削除するコード:
viewModel.score.observe(this, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- アプリをクリーン、再ビルド、実行してから、ゲームをプレイします。現在の単語とスコアがゲーム画面でフォーマットされています。
これで、アプリで LiveData
と ViewModel
をデータ バインディングと統合しました。これにより、レイアウト内のビューは、フラグメントのクリック ハンドラを使用することなく、ViewModel
と直接通信できます。また、LiveData
オブザーバー メソッドを使用せずに、LiveData
オブジェクトをデータ バインディング ソースとして使用して、データの変更について UI に自動的に通知しました。
Android Studio プロジェクト: GuessTheWord
- データ バインディング ライブラリは、
ViewModel
やLiveData
などの 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 に自動的に通知できます。- ビューを
ViewModel
のLiveData
オブジェクトに直接バインドできます。ViewModel
のLiveData
が変更されると、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()
などのイベントが発生したとき- アクティビティがバックグラウンドに移動したとき
次のレッスンに進む:
このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。