この Codelab は、Android Kotlin の基礎コースの一部です。Codelab を順番に進めていくと、このコースを最大限に活用できます。すべてのコース Codelab は Android Kotlin の基礎 Codelab ランディング ページに掲載されています。
はじめに
このレッスンの前の Codelab では、GuessTheWord アプリのコードを改善しました。アプリは ViewModel
オブジェクトを使用するようになったため、アプリデータは画面の回転やキーボードの可用性の変更などのデバイス構成の変更に耐えます。また、オブザーバブルな LiveData
を追加しました。これにより、実測データが変更されるとビューが自動的に通知されます。
この Codelab では、引き続き GuessTheWord アプリを使用します。レイアウトのビューが ViewModel
オブジェクトと直接通信するように、アプリの ViewModel
クラスにビューをバインドします。(これまで、ビューはアプリのフラグメントを介して、ビューが ViewModel
と間接的に通信していました)。データ バインディングを ViewModel
オブジェクトと統合すると、アプリのフラグメントにクリック ハンドラは必要なくなるため、削除します。
また、LiveData
オブザーバー メソッドを使用せずに、データ バインディング ソースとして LiveData
を使用し、データの変更について UI に通知するように GuessTheWord アプリを変更します。
前提となる知識
- Kotlin で基本的な Android アプリを作成する方法。
- アクティビティとフラグメントのライフサイクルの仕組み
- アプリで
ViewModel
オブジェクトを使用する方法 LiveData
を使用してViewModel
にデータを保存する方法。LiveData
データの変化を観察するオブザーバー メソッドを追加する方法
学習内容
- データ バインディング ライブラリの要素の使用方法。
ViewModel
をデータ バインディングと統合する方法。LiveData
をデータ バインディングと統合する方法。- リスナー バインディングを使用して、フラグメント内のクリック リスナーを置き換える方法。
- データ バインディング式に文字列形式を追加する方法
演習内容
- GuessTheWord レイアウトのビューは、UI コントローラ(フラグメント)を使用して
ViewModel
オブジェクトと間接的に情報をやり取りします。この Codelab では、ビューをViewModel
オブジェクトと直接通信するように、アプリのビューをViewModel
オブジェクトにバインドします。 LiveData
をデータ バインディング ソースとして使用するようにアプリを変更します。この変更の後は、LiveData
オブジェクトがデータの変更について UI に通知し、LiveData
オブザーバー メソッドは不要になります。
レッスン 5 の Codelab では、スターター コードから GuessTheWord アプリを開発します。GuessTheWord は 2 人構成のキャラクター スタイル ゲームで、プレーヤーが協力して最高スコアを獲得します。
1 人目のプレーヤーはアプリで単語を確認し、2 人目のプレーヤーに単語を表示しないように順番に実行します。2 人目のプレーヤーがその単語を推測します。
ゲームをプレイするには、最初のプレーヤーがデバイスでアプリを開き、以下のスクリーンショットに示すように「ギター」などの単語が表示されます。
1 人目のプレーヤーが単語を実際に操作し、単語そのものを言わないように注意します。
- 2 人目のプレーヤーが単語を正しく推測したら、[OK] ボタンを押すと、カウントが 1 つ増えて次の単語が表示されます。
- 2 人目のプレーヤーが単語を推測できない場合は、[スキップ] ボタンを押すと、カウントが 1 つ減って次の単語に進みます。
- ゲームを終了するには、[ゲームを終了] ボタンを押します。(この機能は、シリーズの最初の Codelab のスターター コードにはありません)。
この Codelab では、ViewModel
オブジェクトで LiveData
とデータ バインディングを統合して、GuesTheWord アプリを改善します。これにより、レイアウト内のビューと ViewModel
オブジェクト間の通信が自動的に行われ、LiveData
を使用してコードを簡素化できます。
タイトル画面 | ゲーム画面 | スコア画面 |
このタスクでは、この Codelab のスターター コードを見つけて実行します。前の Codelab で作成した GuessTheWord アプリをスターター コードとして使用することも、スターター アプリをダウンロードすることもできます。
- (省略可)前の Codelab のコードを使用していない場合は、この Codelab のスターター コードをダウンロードします。コードを解凍し、Android Studio でプロジェクトを開きます。
- アプリを実行して、ゲームをプレイします。
- [OK] ボタンによって次の単語が表示され、スコアが 1 つ上がり、[Skip] ボタンによって次の単語が表示され、スコアが 1 つ減ります。[ゲームを終了] ボタンをクリックするとゲームが終了します。
- すべての単語を順番に順番に見ていくと、アプリが自動的にスコア画面に移動することがわかります。
前の Codelab では、GessTheWord アプリのビューにアクセスするための型安全な方法としてデータ バインディングを使用していました。しかし、データ バインディングの真の力は、名前が示すように、データをアプリのビュー オブジェクトに直接バインドすることです。
現在のアプリ アーキテクチャ
アプリでは、ビューは XML レイアウトで定義され、そのビューのデータは ViewModel
オブジェクトに保持されます。各ビューとそれに対応する ViewModel
の間には、ビュー間のリレーとして機能する UI コントローラがあります。
次に例を示します。
- [Got it] ボタンは、
game_fragment.xml
レイアウト ファイルではButton
ビューとして定義されます。 - ユーザーが [OK] ボタンをタップすると、
GameFragment
フラグメントのクリック リスナーがGameViewModel
内の対応するクリック リスナーを呼び出します。 - スコアは
GameViewModel
で更新されます。
Button
ビューと GameViewModel
は、直接通信することはなく、GameFragment
のクリック リスナーを必要とします。
データ バインディングに渡される ViewModel
具体的には、レイアウト内のビューが ViewModel
オブジェクトのデータと直接通信することで、UI コントローラを経由する仕組みが簡略化されます。
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
を割り当てます。このコードは、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
ステップ 2: イベント処理にリスナー バインディングを使用する
リスナー バインディングは、onClick()
、onZoomIn()
、onZoomOut()
などのイベントがトリガーされたときに実行されるバインディング式です。リスナー バインディングはラムダ式として記述される。
データ バインディングによってリスナーが作成され、ビューにリスナーが設定されます。Listened イベントが発生すると、リスナーがラムダ式を評価します。リスナー バインディングは、Android Gradle プラグイン バージョン 2.0 以降で機能する。詳細については、レイアウトとバインディング式をご覧ください。
このステップでは、GameFragment
のクリック リスナーを game_fragment.xml
ファイル内のリスナー バインディングに置き換えます。
game_fragment.xml
で、onClick
属性をskip_button
に追加します。バインディング式を定義し、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
で、onClick
属性をplay_again_button
に追加します。リスナー バインディングを定義し、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
オブジェクトには空の文字列が表示されます。
onCreateView()
のGameFragment
で、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
属性をスコアテキスト ビューに追加します。text
属性にscoreViewModel.score
を割り当てます。score
は整数であるため、String.valueOf()
を使用して文字列に変換します。
<TextView
android:id="@+id/score_text"
...
android:text="@{String.valueOf(scoreViewModel.score)}"
... />
ScoreFragment
で、scoreViewModel
を初期化した後、現在のアクティビティを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
データ バインディングが機能するように、現在のアクティビティ(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 ランディング ページをご覧ください。