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)
}
}
currentScrambledWord
LiveData
のオブザーバーを接続します。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()
メソッドの最後で、currentWordCount
LiveData
のオブザーバーをアタッチします。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 リファレンス
- データ バインディング
- 双方向データ バインディング
ブログ投稿