1. 始める前に
前の Codelab では、ViewModel を使用してアプリデータを保存する方法を学習しました。ViewModel を使用すると、構成変更後にアプリのデータを引き継ぐことができます。この Codelab では、LiveData を ViewModel のデータと統合する方法を学びます。
LiveData クラスは Android アーキテクチャ コンポーネントの一部でもあり、監視可能なデータホルダー クラスです。
前提条件
- GitHub からソースコードをダウンロードして Android Studio で開けること
- Kotlin でアクティビティとフラグメントを使って基本的な Android アプリを作成し実行できること
- アクティビティとフラグメントのライフサイクルに対する理解
ViewModelを使用してデバイスの構成変更後も UI データを維持できること- ラムダ式を記述できること
学習内容
- アプリで
LiveDataとMutableLiveDataを使用する方法 ViewModelに保存されているデータをLiveDataでカプセル化する方法LiveData.での変更を監視するオブザーバー メソッドを追加する方法- レイアウト ファイル内にバインディング式を記述する方法
作成するアプリの概要
- Unscramble アプリで、アプリのデータ(単語、単語カウント、スコア)に
LiveDataを使用します。 - データが変更されたときに通知を受け取るオブザーバー メソッドを追加して、スクランブルされた単語のテキストビューを自動的に更新します。
- レイアウト ファイル内にバインディング式を記述します。これは、バックの
LiveDataが変更されたときにトリガーされます。スコア、単語カウント、スクランブルされた単語のテキストビューは自動的に更新されます。
必要なもの
- Android Studio がインストールされているパソコン
- 前の Codelab の解答コード(
ViewModelを使った Unscramble アプリ)
この Codelab のスターター コードをダウンロードする
この Codelab では、前の Codelab(ViewModel にデータを保存する)で作成した Unscramble アプリをスターター コードとして使用します。
2. スターター アプリの概要
この Codelab では、前の Codelab で詳しく学習した Unscramble 解答コードを使用します。アプリには、プレーヤーがスクランブル解除する単語が表示されます。プレーヤーは、何度でも回答することができます。現在の単語、プレーヤーのスコア、単語カウントなどのアプリデータは ViewModel に保存されます。ただし、アプリの UI には新しいスコアと単語カウントの値が反映されません。この Codelab では、LiveData を使って欠けている機能を実装します。

3.LiveData について
LiveData はライフサイクル対応の監視可能なデータホルダー クラスです。
LiveData には次のような特性があります。
LiveDataにはデータが保持されます。LiveDataは、あらゆる種類のデータに使用できるラッパーです。LiveDataは監視可能です。つまり、LiveDataオブジェクトに保持されているデータが変更されるとオブザーバーに通知されます。LiveDataはライフサイクルに対応しています。オブザーバーをLiveDataに接続すると、オブザーバーはLifecycleOwner(通常はアクティビティまたはフラグメント)に関連付けられます。LiveDataにより、ライフサイクルの状態がアクティブ(STARTEDやRESUMEDなど)なオブザーバーのみが更新されます。LiveDataと監視について詳しくは、こちらをご覧ください。
スターター コードの UI 更新処理
スターター コードでは、新しいスクランブルされた単語を UI に表示しようとするたびに、updateNextWordOnScreen() メソッドが明示的に呼び出されます。このメソッドは、ゲームを初期化したとき、およびプレーヤーが [Submit] ボタンまたは [Skip] ボタンを押したときに呼び出されます。このメソッドは、onViewCreated()、restartGame()、onSkipWord()、onSubmitWord() メソッドから呼び出されます。Livedata を使用すると、UI を更新するために複数の場所でこのメソッドを呼び出す必要がなくなります。オブザーバーで一度だけ呼び出します。
4. 現在のスクランブルされた単語に LiveData を追加する
このタスクでは、GameViewModel にある現在の単語を LiveData に変換することによって、あらゆるデータを LiveData, でラップする方法を学びます。後のタスクでは、これらの LiveData オブジェクトにオブザーバーを追加し、LiveData を監視する方法を学びます。
MutableLiveData
MutableLiveData は LiveData の変更可能なバージョンです。つまり、その中に保存されているデータの値は変更できます。
GameViewModelで変数_currentScrambledWordの型をMutableLiveData<String>に変更します。LiveDataとMutableLiveDataは汎用のクラスであるため、保持するデータの種類を指定する必要があります。_currentScrambledWordの変数の型をvalに変更します。LiveData/MutableLiveDataオブジェクトの値は変更されず、オブジェクト内に保存されているデータのみが変更されるためです。
private val _currentScrambledWord = MutableLiveData<String>()
- バッキング フィールド
currentScrambledWord型は不変であるため、LiveData<String>に変更します。Android Studio にエラーが表示されますが、これは次のステップで修正します。
val currentScrambledWord: LiveData<String>
get() = _currentScrambledWord
LiveDataオブジェクト内のデータにアクセスするには、valueプロパティを使用します。getNextWord()メソッドのGameViewModelにあるelseブロック内の_currentScrambledWordという参照を_currentScrambledWord.valueに変更します。
private fun getNextWord() {
...
} else {
_currentScrambledWord.value = String(tempWord)
...
}
}
5. オブザーバーを LiveData オブジェクトに接続する
このタスクでは、アプリ コンポーネント GameFragment にオブザーバーを設定します。追加したオブザーバーにより、アプリのデータ currentScrambledWord に対する変更が監視されます。LiveData はライフサイクル対応です。つまり、アクティブなライフサイクルの状態にあるオブザーバーのみを更新します。したがって、GameFragment のオブザーバーには、GameFragment が STARTED 状態または RESUMED 状態の場合にのみ通知されます。
GameFragmentで、メソッドupdateNextWordOnScreen()とそのメソッドの呼び出しをすべて削除します。オブザーバーをLiveDataに接続するため、このメソッドは必要ありません。onSubmitWord()で、空のif-elseブロックを次のように変更します。変更後のメソッドは次のようになります。
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (!viewModel.nextWord()) {
showFinalScoreDialog()
}
} else {
setErrorTextField(true)
}
}
currentScrambledWordLiveDataのオブザーバーを接続します。GameFragmentのコールバックonViewCreated()の最後で、currentScrambledWordのobserve()メソッドを呼び出します。
// Observe the currentScrambledWord LiveData.
viewModel.currentScrambledWord.observe()
Android Studio に、パラメータが足らないというエラーが表示されます。このエラーは次のステップで修正します。
viewLifecycleOwnerを最初のパラメータとしてobserve()メソッドに渡します。viewLifecycleOwnerはフラグメントの View のライフサイクルを表します。このパラメータにより、LiveDataがGameFragmentライフサイクルを認識し、GameFragmentがアクティブな状態(STARTEDまたはRESUMED)の場合にのみオブザーバーに通知するようになります。- 関数パラメータとして
newWordを持つラムダを 2 番目のパラメータとして追加します。newWordには、スクランブルされた新しい単語の値が入ります。
// Observe the scrambledCharArray LiveData, passing in the LifecycleOwner and the observer.
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
{ newWord ->
})
ラムダ式は、宣言されない匿名関数であり、式としてその場で渡されます。ラムダ式は常に波かっこ「{ }」で囲まれています。
- このラムダ式の関数本体では、スクランブルされた単語のテキストビューに
newWordを代入します。
// Observe the scrambledCharArray LiveData, passing in the LifecycleOwner and the observer.
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
{ newWord ->
binding.textViewUnscrambledWord.text = newWord
})
- コンパイルしてアプリを実行します。ゲームアプリは、前とまったく同じように動作しますが、スクランブルされた単語のテキストビューは、
updateNextWordOnScreen()メソッドではなくLiveDataオブザーバーで自動的に更新されます。
6. オブザーバーをスコアと単語カウントに接続する
前のタスクと同様に、このタスクでは、アプリ内の他のデータ(スコアと単語カウント)に LiveData を追加して、ゲーム中にスコアと単語カウントの正しい値で UI が更新されるようにします。
ステップ 1: LiveData でスコアと単語カウントをラップする
GameViewModelで、_scoreクラス変数と_currentWordCountクラス変数の型をvalに変更します。- 変数
_scoreと変数_currentWordCountのデータ型をMutableLiveDataに変更し、0に初期化します。 - バッキング フィールドの型を
LiveData<Int>に変更します。
private val _score = MutableLiveData(0)
val score: LiveData<Int>
get() = _score
private val _currentWordCount = MutableLiveData(0)
val currentWordCount: LiveData<Int>
get() = _currentWordCount
GameViewModelのreinitializeData()メソッドの先頭で、_scoreと_currentWordCountの参照をそれぞれ_score.valueと_currentWordCount.valueに変更します。
fun reinitializeData() {
_score.value = 0
_currentWordCount.value = 0
wordsList.clear()
getNextWord()
}
GameViewModelのnextWord()メソッド内で、_currentWordCountの参照を_currentWordCount.value!!に変更します。
fun nextWord(): Boolean {
return if (_currentWordCount.value!! < MAX_NO_OF_WORDS) {
getNextWord()
true
} else false
}
GameViewModelのincreaseScore()メソッド内とgetNextWord()メソッド内で、_scoreと_currentWordCountの参照をそれぞれ_score.valueと_currentWordCount.valueに変更します。_scoreが整数ではなくLiveDataになったため、Android Studio にエラーが表示されます。これは次のステップで修正します。plus()Kotlin 関数を使用して_score値を増やします。これにより、null 安全な状態で加算が実行されます。
private fun increaseScore() {
_score.value = (_score.value)?.plus(SCORE_INCREASE)
}
- 同様に、
inc()Kotlin 関数を使用して、null 安全な状態で値がインクリメントされます。
private fun getNextWord() {
...
} else {
_currentScrambledWord.value = String(tempWord)
_currentWordCount.value = (_currentWordCount.value)?.inc()
wordsList.add(currentWord)
}
}
GameFragmentで、valueプロパティを使用してscoreの値にアクセスします。showFinalScoreDialog()メソッド内で、viewModel.scoreをviewModel.score.valueに変更します。
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score.value))
...
.show()
}
ステップ 2: オブザーバーをスコアと単語カウントに接続する
このアプリでは、スコアと単語カウントが更新されません。このタスクで LiveData オブザーバーを使用して更新します。
GameFragmentのonViewCreated()メソッドで、スコアと単語カウントのテキストビューを更新するコードを削除します。
以下を削除します。
binding.score.text = getString(R.string.score, 0)
binding.wordCount.text = getString(R.string.word_count, 0, MAX_NO_OF_WORDS)
onViewCreated()メソッドの最後にあるGameFragmentで、scoreのオブザーバーをアタッチします。viewLifecycleOwnerをオブザーバーの最初のパラメータとして、ラムダ式を 2 番目のパラメータとして渡します。ラムダ式では、新しいスコアをパラメータとして渡し、関数本体では、新しいスコアをテキストビューに設定します。
viewModel.score.observe(viewLifecycleOwner,
{ newScore ->
binding.score.text = getString(R.string.score, newScore)
})
onViewCreated()メソッドの最後で、currentWordCountLiveDataのオブザーバーをアタッチします。viewLifecycleOwnerをオブザーバーの最初のパラメータとして、ラムダ式を 2 番目のパラメータとして渡します。ラムダ式では、新しい単語カウントをパラメータとして渡し、関数本体では、新しい単語カウントをMAX_NO_OF_WORDSとともにテキストビューに設定します。
viewModel.currentWordCount.observe(viewLifecycleOwner,
{ newWordCount ->
binding.wordCount.text =
getString(R.string.word_count, newWordCount, MAX_NO_OF_WORDS)
})
新しいオブザーバーは、ライフサイクル所有者(GameFragment)の存続期間中に、ViewModel 内のスコアと単語カウントの値が変更されるとトリガーされます。
- アプリを実行して動作を確認します。ゲームを数語プレイします。前と同じようにスコアと単語カウントが画面で正しく更新されます。コード内の条件によっては、これらのテキストビューを更新していないことを確認してください。
scoreとcurrentWordCountはLiveDataであり、バックの値が変更されたときに、対応するオブザーバーが自動的に呼び出されています。

7. LiveData をデータ バインディングで使用する
前のタスクでは、アプリでコード内のデータ変更をリッスンしています。同様に、アプリでレイアウトからのデータ変更をリッスンできます。データ バインディングを使用すると、監視可能な LiveData 値が変更されたときに、バインドされているレイアウト内の UI 要素にも通知され、UI をレイアウト内から更新できます。
コンセプト: データ バインディング
前の Codelab では、ビュー バインディング(一方向バインディング)を説明しました。ビューをコードにバインドできますが、その逆はできません。
ビュー バインディングの復習
ビュー バインディングを使用すると、コード内でビューに簡単にアクセスできます。この機能により、XML レイアウト ファイルごとにバインディング クラスが生成されます。バインディング クラスのインスタンスには、対応するレイアウトで ID を持つすべてのビューへの直接参照が入っています。たとえば、Unscramble アプリはビュー バインディングを使用しているため、生成されたバインディング クラスを使用してコード内でビューを参照できます。
例:
binding.textViewUnscrambledWord.text = newWord
binding.score.text = getString(R.string.score, newScore)
binding.wordCount.text =
getString(R.string.word_count, newWordCount, MAX_NO_OF_WORDS)
ビュー バインディングを使用しても、ビュー内のアプリデータ(レイアウト ファイル)を参照できません。データ バインディングを使用すると、これが可能になります。
データ バインディング
データ バインディング ライブラリは Android Jetpack ライブラリの一部でもあります。データ バインディングでは、宣言的な形式を使用して、レイアウト内の UI コンポーネントをアプリのデータソースにバインドします。詳しくは、この Codelab の後半で説明します。
簡単に言うと、データ バインディングとは、ビューに対する(コードの)データのバインディングに、ビュー バインディング(コードに対するビューのバインド)を加えたものです。
UI コントローラでのビュー バインディングの使用例
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
レイアウト ファイルでのデータ バインディングの使用例
android:text="@{gameViewModel.currentScrambledWord}"
上記の例は、データ バインディング ライブラリを使用して、レイアウト ファイルでアプリデータをビューやウィジェットに直接割り当てる方法を示しています。代入式で @{} 構文を使用することに注意してください。
データ バインディングを使用する主な利点は、アクティビティ内の多くの UI フレームワーク呼び出しを削除できるという点です。これにより、アクティビティがシンプルかつ簡単になります。また、アプリのパフォーマンスが向上し、メモリリークと null ポインタ例外の発生を防ぐこともできます。
ステップ 1: ビュー バインディングをデータ バインディングに変更する
build.gradle(Module)ファイルのbuildFeaturesセクションにあるdataBindingプロパティを有効にします。
次の内容を
buildFeatures {
viewBinding = true
}
次のように置き換えます。
buildFeatures {
dataBinding = true
}
Android Studio にプロンプトが表示されたら、Gradle 同期を実行します。
- Kotlin プロジェクトでデータ バインディングを使用するには、
kotlin-kaptプラグインを適用する必要があります。このステップはbuild.gradle(Module)ファイルですでに完了しています。
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
以上のステップにより、アプリのすべてのレイアウト XML ファイルに自動的にバインディング クラスが生成されます。レイアウト ファイル名が activity_main.xml の場合、自動生成クラスの名前は ActivityMainBinding になります。
ステップ 2: レイアウト ファイルをデータ バインディング レイアウトに変換する
データ バインディングのレイアウト ファイルは少し異なっており、<layout> のルートタグから始まり、その後に <data> 要素(省略可)と view ルート要素が続きます。このビュー要素は、非バインディング レイアウト ファイルであればルート要素だったものです。
game_fragment.xmlを開き、[code] タブを選択します。- レイアウトをデータ バインディングのレイアウトに変換するために、ルート要素を
<layout>タグで囲みます。また、名前空間の定義(xmlns:で始まる属性)を新しいルート要素に移動させる必要もあります。ルート要素の上の<layout>タグ内に<data></data>タグを追加します。Android Studio には、これを簡単に行う方法があります。すなわち、ルート要素(ScrollView)を右クリックし、[Show Context Actions] > [Convert to data binding layout] を選択します。

- レイアウトは次のようになります。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
...
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</layout>
GameFragmentのonCreateView()メソッドの先頭で、binding変数のインスタンス化にデータ バインディングを使用するよう変更します。
次の内容を
binding = GameFragmentBinding.inflate(inflater, container, false)
次のように置き換えます。
binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false)
- コードをコンパイルします。問題なくコンパイルできるはずです。アプリでデータ バインディングが使用され、レイアウト内のビューからアプリデータにアクセスできるようになりました。
8. データ バインディング変数を追加する
このタスクでは、viewModel からアプリデータにアクセスするために、レイアウト ファイルにプロパティを追加します。コードでレイアウト変数を初期化します。
game_fragment.xmlの<data>タグに<variable>という名前の子タグを追加して、gameViewModelというGameViewModel型のプロパティを宣言します。これを使用してViewModelのデータをレイアウトにバインドします。
<data>
<variable
name="gameViewModel"
type="com.example.android.unscramble.ui.game.GameViewModel" />
</data>
gameViewModel の型にパッケージ名が含まれていることに注意してください。このパッケージ名がアプリ内のパッケージ名と一致するようにしてください。
gameViewModel宣言の下で、<data>タグ内にInteger型の変数を追加し、maxNoOfWordsという名前を付けます。これを使用して ViewModel の変数にバインドし、ゲームごとの単語数を保存します。
<data>
...
<variable
name="maxNoOfWords"
type="int" />
</data>
GameFragmentのonViewCreated()メソッドの先頭で、レイアウト変数gameViewModelとmaxNoOfWordsを初期化します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.gameViewModel = viewModel
binding.maxNoOfWords = MAX_NO_OF_WORDS
...
}
LiveDataはライフサイクル対応かつ監視可能なため、ライフサイクル所有者をレイアウトに渡す必要があります。GameFragmentのonViewCreated()メソッド内で、バインディング変数の初期化の下に次のコードを追加します。
// 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
LiveData オブザーバーを実装する際に、同じような機能を実装したことを思い出してください。パラメータの一つとして viewLifecycleOwner を LiveData オブザーバーに渡しました。
9. バインディング式を使用する
バインディング式は、レイアウト内のレイアウト プロパティを参照する属性プロパティ(android:text など)で記述されます。レイアウト プロパティは、<variable> タグによってデータ バインディングのレイアウト ファイルの上部で宣言されます。依存する変数が変更されると、「DB ライブラリ」によりバインディング式が実行されます(これによりビューが更新されます)。この変更検出は、データ バインディング ライブラリを使用するとコストなしで使用できる優れた最適化機能です。
バインディング式の構文
バインディング式は、「@」記号で始め、波かっこ「{}」で囲います。次の例では、TextView のテキストが user 変数の firstName プロパティに設定されます。
例:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
ステップ 1: 現在の単語にバインディング式を追加する
このステップでは、現在の単語のテキストビューを ViewModel の LiveData オブジェクトにバインドします。
game_fragment.xmlで、textView_unscrambled_wordテキストビューにtext属性を追加します。新しいレイアウト変数gameViewModelを使用して、@{gameViewModel.currentScrambledWord}をtext属性に割り当てます。
<TextView
android:id="@+id/textView_unscrambled_word"
...
android:text="@{gameViewModel.currentScrambledWord}"
.../>
GameFragmentで、currentScrambledWordのLiveDataオブザーバー コードを削除します。フラグメントのオブザーバー コードは不要になりました。レイアウトには、LiveDataに対する変更の更新が直接反映されます。
以下を削除します。
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
{ newWord ->
binding.textViewUnscrambledWord.text = newWord
})
- アプリを実行します。アプリは前と同じように動作するはずです。しかし、スクランブルされた単語のテキストビューでは、
LiveDataオブザーバーではなく、バインディング式を使って UI を更新しています。
ステップ 2: スコアと単語カウントにバインディング式を追加する
データ バインディング式内のリソース
データ バインディング式からは、次の構文でアプリリソースを参照できます。
例:
android:padding="@{@dimen/largePadding}"
上記の例では、padding 属性に dimen.xml リソース ファイルの largePadding の値が割り当てられています。
リソース パラメータとしてレイアウト プロパティを渡すこともできます。
例:
android:text="@{@string/example_resource(user.lastName)}"
strings.xml
<string name="example_resource">Last Name: %s</string>
上記の例では、example_resource は %s プレースホルダを持つ文字列リソースです。バインディング式内でリソース パラメータとして user.lastName を渡します。ここで user はレイアウト変数です。
このステップでは、スコアと単語カウントのテキストビューにバインディング式を追加し、リソース パラメータを渡します。このステップは、上記の textView_unscrambled_word のステップと似ています。
game_fragment.xmlで、word_countテキストビューのtext属性を次のバインディング式で更新します。word_count文字列リソースを使用し、リソース パラメータとしてgameViewModel.currentWordCountとmaxNoOfWordsを渡します。
<TextView
android:id="@+id/word_count"
...
android:text="@{@string/word_count(gameViewModel.currentWordCount, maxNoOfWords)}"
.../>
scoreテキストビューのtext属性を次のバインディング式で更新します。score文字列リソースを使用し、リソース パラメータとしてgameViewModel.scoreを渡します。
<TextView
android:id="@+id/score"
...
android:text="@{@string/score(gameViewModel.score)}"
... />
GameFragmentからLiveDataオブザーバーを削除します。これは必要なくなり、対応するLiveDataが変更されると、バインディング式によって UI が更新されます。
以下を削除します。
viewModel.score.observe(viewLifecycleOwner,
{ newScore ->
binding.score.text = getString(R.string.score, newScore)
})
viewModel.currentWordCount.observe(viewLifecycleOwner,
{ newWordCount ->
binding.wordCount.text =
getString(R.string.word_count, newWordCount, MAX_NO_OF_WORDS)
})
- アプリを実行して、数語プレイします。これで、コードで
LiveDataとバインディング式が使用されて UI が更新されるようになりました。

お疲れさまでした。LiveData オブザーバーで LiveData を使用する方法と、バインディング式で LiveData を使用する方法を学びました。
10. TalkBack を有効にして Unscramble アプリをテストする
このコースで学習してきたとおり、できるだけ多くのユーザーが利用できるアプリを作成する必要があります。一部のユーザーは、TalkBack を使用してアプリを操作する場合があります。TalkBack は、Android デバイスに組み込まれている Google スクリーン リーダーです。TalkBack から音声フィードバックが出力されるので、画面を見ずにデバイスを使用できます。
TalkBack を有効にして、プレーヤーがゲームをプレイできることを確認しましょう。
- デバイスで TalkBack を有効にするには、こちらの手順を実施します。
- Unscramble アプリに戻ります。
- こちらの手順に沿って、TalkBack でアプリを探します。右にスワイプして画面要素間を順番に移動し、左にスワイプして逆に移動します。選択するには、任意の場所をダブルタップします。スワイプ操作でアプリのすべての要素にアクセスできることを確認します。
- TalkBack ユーザーが画面上の各アイテムに移動できることを確認します。
- TalkBack がスクランブルされた単語を通常の単語のように読み上げようとしていることがわかります。これは実際の単語ではないため、プレーヤーが混乱する可能性があります。
- ユーザー エクスペリエンスを改善する方法として、スクランブルされた単語の個々の文字を TalkBack で読み上げる方法があります。
GameViewModelで、スクランブルされた単語StringをSpannable文字列に変換します。Spannable 文字列とは、追加情報が付加された文字列です。ここでは、テキスト読み上げエンジンがスクランブルされた単語をそのまま一文字ずつ読み上げられるように、文字列をTYPE_VERBATIMのTtsSpanに関連付けます。 GameViewModelで、次のコードを使用してcurrentScrambledWord変数の宣言を変更します。
val currentScrambledWord: LiveData<Spannable> = Transformations.map(_currentScrambledWord) {
if (it == null) {
SpannableString("")
} else {
val scrambledWord = it.toString()
val spannable: Spannable = SpannableString(scrambledWord)
spannable.setSpan(
TtsSpan.VerbatimBuilder(scrambledWord).build(),
0,
scrambledWord.length,
Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
spannable
}
}
この変数は LiveData<String> から LiveData<Spannable> になりました。動作の詳細をすべて理解する必要はありませんが、実装では LiveData 変換を使用して、スクランブルされた時点の単語 String をユーザー補助サービスで適切に処理できる Spannable 文字列に変換しています。次の Codelab では、LiveData 変換について学習します。これにより、対応する LiveData の値に基づいて別の LiveData インスタンスを返すことができるようになります。
- Unscramble アプリを実行し、TalkBack でアプリを使用します。スクランブルされた単語の個々の文字が TalkBack で読み上げられるはずです。
アプリのユーザー補助機能を強化する方法について詳しくは、こちらの原則をご確認ください。
11. 使用しなくなったコードを削除する
解答コードで不要になったコードを削除するのは良いことです。これによりコードの管理が容易になり、新しいチームメンバーもコードを理解しやすくなります。
GameFragmentのgetNextScrambledWord()メソッドとonDetach()メソッドを削除します。GameViewModelのonCleared()メソッドを削除します。- ソースファイルの先頭にある使用されていないインポートを削除します。これはグレー表示されます。
ログ ステートメントも不要になりました。コードから削除しても構いません。
- (省略可)
ViewModelのライフサイクルを理解するために前の Codelab でソースファイル(GameFragment.ktとGameViewModel.kt)に追加したLogステートメントを削除します。
12. 解答コード
この Codelab の解答コードは、以下に示すプロジェクトにあります。
- プロジェクト用に提供されている GitHub リポジトリ ページに移動します。
- ブランチ名が Codelab で指定されたブランチ名と一致していることを確認します。たとえば、次のスクリーンショットでは、ブランチ名は main です。

- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ポップアップが表示されます。

- ポップアップで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ちます。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで、[Open] をクリックします。

注: Android Studio がすでに開いている場合は、メニューから [File] > [Open] を選択します。

- ファイル ブラウザで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開かれるまで待ちます。
- 実行ボタン
をクリックして、アプリをビルドし、実行します。期待どおりにビルドされることを確認します。
13. まとめ
LiveDataにはデータが保持されます。LiveDataはあらゆる種類のデータに使用できるラッパーです。LiveDataは監視可能です。つまり、LiveDataオブジェクトに保持されているデータが変更されるとオブザーバーに通知されます。LiveDataはライフサイクルに対応しています。オブザーバーをLiveDataに接続すると、オブザーバーはLifecycleOwner(通常はアクティビティまたはフラグメント)に関連付けられます。LiveData により、ライフサイクルの状態がアクティブ(STARTEDやRESUMEDなど)なオブザーバーのみが更新されます。LiveDataと監視について詳しくは、こちらをご覧ください。- アプリでは、データ バインディングとバインディング式を使用してレイアウトからの LiveData の変更をリッスンできます。
- バインディング式は、レイアウト内のレイアウト プロパティを参照する属性プロパティ(
android:textなど)で記述されます。
14. 関連リンク
- LiveData の概要
- LiveData observer API リファレンス
- データ バインディング
- 双方向データ バインディング
ブログ投稿