ViewModel で LiveData を使用する

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 を使って欠けている機能を実装します。

a20e6e45e0d5dc6f.png

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 の変更可能なバージョンです。つまり、その中に保存されているデータの値は変更できます。

  1. GameViewModel で変数 _currentScrambledWord の型を MutableLiveData<String> に変更します。LiveData と MutableLiveData は汎用のクラスであるため、保持するデータの種類を指定する必要があります。
  2. _currentScrambledWord の変数の型を val に変更します。LiveData / MutableLiveData オブジェクトの値は変更されず、オブジェクト内に保存されているデータのみが変更されるためです。
private val _currentScrambledWord = MutableLiveData<String>()
  1. バッキング フィールド currentScrambledWord 型は不変であるため、LiveData<String> に変更します。Android Studio にエラーが表示されますが、これは次のステップで修正します。
val currentScrambledWord: LiveData<String>
   get() = _currentScrambledWord
  1. 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 状態の場合にのみ通知されます。

  1. GameFragment で、メソッド updateNextWordOnScreen() とそのメソッドの呼び出しをすべて削除します。オブザーバーを LiveData に接続するため、このメソッドは必要ありません。
  2. 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)
    }
}
  1. currentScrambledWord LiveData のオブザーバーを接続します。GameFragment のコールバック onViewCreated() の最後で、currentScrambledWord の observe() メソッドを呼び出します。
// Observe the currentScrambledWord LiveData.
viewModel.currentScrambledWord.observe()

Android Studio に、パラメータが足らないというエラーが表示されます。このエラーは次のステップで修正します。

  1. viewLifecycleOwner を最初のパラメータとして observe() メソッドに渡します。viewLifecycleOwner はフラグメントの View のライフサイクルを表します。このパラメータにより、LiveData が GameFragment ライフサイクルを認識し、GameFragment がアクティブな状態(STARTED または RESUMED)の場合にのみオブザーバーに通知するようになります。
  2. 関数パラメータとして newWord を持つラムダを 2 番目のパラメータとして追加します。newWord には、スクランブルされた新しい単語の値が入ります。
// Observe the scrambledCharArray LiveData, passing in the LifecycleOwner and the observer.
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
   { newWord ->
   })

ラムダ式は、宣言されない匿名関数であり、式としてその場で渡されます。ラムダ式は常に波かっこ「{ }」で囲まれています。

  1. このラムダ式の関数本体では、スクランブルされた単語のテキストビューに newWord を代入します。
// Observe the scrambledCharArray LiveData, passing in the LifecycleOwner and the observer.
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
   { newWord ->
       binding.textViewUnscrambledWord.text = newWord
   })
  1. コンパイルしてアプリを実行します。ゲームアプリは、前とまったく同じように動作しますが、スクランブルされた単語のテキストビューは、updateNextWordOnScreen() メソッドではなく LiveData オブザーバーで自動的に更新されます。

6. オブザーバーをスコアと単語カウントに接続する

前のタスクと同様に、このタスクでは、アプリ内の他のデータ(スコアと単語カウント)に LiveData を追加して、ゲーム中にスコアと単語カウントの正しい値で UI が更新されるようにします。

ステップ 1: LiveData でスコアと単語カウントをラップする

  1. GameViewModel で、_score クラス変数と _currentWordCount クラス変数の型を val に変更します。
  2. 変数 _score と変数 _currentWordCount のデータ型を MutableLiveData に変更し、0 に初期化します。
  3. バッキング フィールドの型を LiveData<Int> に変更します。
private val _score = MutableLiveData(0)
val score: LiveData<Int>
   get() = _score

private val _currentWordCount = MutableLiveData(0)
val currentWordCount: LiveData<Int>
   get() = _currentWordCount
  1. GameViewModel の reinitializeData() メソッドの先頭で、_score と _currentWordCount の参照をそれぞれ _score.value と _currentWordCount.value に変更します。
fun reinitializeData() {
   _score.value = 0
   _currentWordCount.value = 0
   wordsList.clear()
   getNextWord()
}
  1. GameViewModel の nextWord() メソッド内で、_currentWordCount の参照を _currentWordCount.value!! に変更します。
fun nextWord(): Boolean {
    return if (_currentWordCount.value!! < MAX_NO_OF_WORDS) {
           getNextWord()
           true
       } else false
   }
  1. GameViewModel の increaseScore() メソッド内と getNextWord() メソッド内で、_score と _currentWordCount の参照をそれぞれ _score.value と _currentWordCount.value に変更します。_score が整数ではなく LiveData になったため、Android Studio にエラーが表示されます。これは次のステップで修正します。
  2. plus() Kotlin 関数を使用して _score 値を増やします。これにより、null 安全な状態で加算が実行されます。
private fun increaseScore() {
    _score.value = (_score.value)?.plus(SCORE_INCREASE)
}
  1. 同様に、inc() Kotlin 関数を使用して、null 安全な状態で値がインクリメントされます。
private fun getNextWord() {
   ...
    } else {
        _currentScrambledWord.value = String(tempWord)
        _currentWordCount.value = (_currentWordCount.value)?.inc()
        wordsList.add(currentWord)
       }
   }
  1. 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 オブザーバーを使用して更新します。

  1. GameFragment の onViewCreated() メソッドで、スコアと単語カウントのテキストビューを更新するコードを削除します。

以下を削除します。

binding.score.text = getString(R.string.score, 0)
binding.wordCount.text = getString(R.string.word_count, 0, MAX_NO_OF_WORDS)
  1. onViewCreated() メソッドの最後にある GameFragment で、score のオブザーバーをアタッチします。viewLifecycleOwner をオブザーバーの最初のパラメータとして、ラムダ式を 2 番目のパラメータとして渡します。ラムダ式では、新しいスコアをパラメータとして渡し、関数本体では、新しいスコアをテキストビューに設定します。
viewModel.score.observe(viewLifecycleOwner,
   { newScore ->
       binding.score.text = getString(R.string.score, newScore)
   })
  1. 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 内のスコアと単語カウントの値が変更されるとトリガーされます。

  1. アプリを実行して動作を確認します。ゲームを数語プレイします。前と同じようにスコアと単語カウントが画面で正しく更新されます。コード内の条件によっては、これらのテキストビューを更新していないことを確認してください。score と currentWordCount は LiveData であり、バックの値が変更されたときに、対応するオブザーバーが自動的に呼び出されています。

80e118245bdde6df.png

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: ビュー バインディングをデータ バインディングに変更する

  1. build.gradle(Module) ファイルの buildFeatures セクションにある dataBinding プロパティを有効にします。

次の内容を

buildFeatures {
   viewBinding = true
}

次のように置き換えます。

buildFeatures {
   dataBinding = true
}

Android Studio にプロンプトが表示されたら、Gradle 同期を実行します。

  1. 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 ルート要素が続きます。このビュー要素は、非バインディング レイアウト ファイルであればルート要素だったものです。

  1. game_fragment.xml を開き、[code] タブを選択します。
  2. レイアウトをデータ バインディングのレイアウトに変換するために、ルート要素を <layout> タグで囲みます。また、名前空間の定義(xmlns: で始まる属性)を新しいルート要素に移動させる必要もあります。ルート要素の上の <layout> タグ内に <data></data> タグを追加します。Android Studio には、これを簡単に行う方法があります。すなわち、ルート要素(ScrollView)を右クリックし、[Show Context Actions] > [Convert to data binding layout] を選択します。

8d48f58c2bdccb52.png

  1. レイアウトは次のようになります。
<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>
  1. GameFragment の onCreateView() メソッドの先頭で、binding 変数のインスタンス化にデータ バインディングを使用するよう変更します。

次の内容を

binding = GameFragmentBinding.inflate(inflater, container, false)

次のように置き換えます。

binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false)
  1. コードをコンパイルします。問題なくコンパイルできるはずです。アプリでデータ バインディングが使用され、レイアウト内のビューからアプリデータにアクセスできるようになりました。

8. データ バインディング変数を追加する

このタスクでは、viewModel からアプリデータにアクセスするために、レイアウト ファイルにプロパティを追加します。コードでレイアウト変数を初期化します。

  1. game_fragment.xml の <data> タグに <variable> という名前の子タグを追加して、gameViewModel という GameViewModel 型のプロパティを宣言します。これを使用して ViewModel のデータをレイアウトにバインドします。
<data>
   <variable
       name="gameViewModel"
       type="com.example.android.unscramble.ui.game.GameViewModel" />
</data>

gameViewModel の型にパッケージ名が含まれていることに注意してください。このパッケージ名がアプリ内のパッケージ名と一致するようにしてください。

  1. gameViewModel 宣言の下で、<data> タグ内に Integer 型の変数を追加し、maxNoOfWords という名前を付けます。これを使用して ViewModel の変数にバインドし、ゲームごとの単語数を保存します。
<data>
   ...
   <variable
       name="maxNoOfWords"
       type="int" />
</data>
  1. GameFragment の onViewCreated() メソッドの先頭で、レイアウト変数 gameViewModel と maxNoOfWords を初期化します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding.gameViewModel = viewModel

   binding.maxNoOfWords = MAX_NO_OF_WORDS
...
}
  1. 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 オブジェクトにバインドします。

  1. game_fragment.xml で、textView_unscrambled_word テキストビューに text 属性を追加します。新しいレイアウト変数 gameViewModel を使用して、@{gameViewModel.currentScrambledWord} を text 属性に割り当てます。
<TextView
   android:id="@+id/textView_unscrambled_word"
   ...
   android:text="@{gameViewModel.currentScrambledWord}"
   .../>
  1. GameFragment で、currentScrambledWord の LiveData オブザーバー コードを削除します。フラグメントのオブザーバー コードは不要になりました。レイアウトには、LiveData に対する変更の更新が直接反映されます。

以下を削除します。

viewModel.currentScrambledWord.observe(viewLifecycleOwner,
   { newWord ->
       binding.textViewUnscrambledWord.text = newWord
   })
  1. アプリを実行します。アプリは前と同じように動作するはずです。しかし、スクランブルされた単語のテキストビューでは、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 のステップと似ています。

  1. 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)}"
   .../>
  1. score テキストビューの text 属性を次のバインディング式で更新します。score 文字列リソースを使用し、リソース パラメータとして gameViewModel.score を渡します。
<TextView
   android:id="@+id/score"
   ...
   android:text="@{@string/score(gameViewModel.score)}"
   ... />
  1. 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)
   })
  1. アプリを実行して、数語プレイします。これで、コードで LiveData とバインディング式が使用されて UI が更新されるようになりました。

7880e60dc0a6f95c.png 9ef2fdf21ffa5c99.png

お疲れさまでした。LiveData オブザーバーで LiveData を使用する方法と、バインディング式で LiveData を使用する方法を学びました。

10. TalkBack を有効にして Unscramble アプリをテストする

このコースで学習してきたとおり、できるだけ多くのユーザーが利用できるアプリを作成する必要があります。一部のユーザーは、TalkBack を使用してアプリを操作する場合があります。TalkBack は、Android デバイスに組み込まれている Google スクリーン リーダーです。TalkBack から音声フィードバックが出力されるので、画面を見ずにデバイスを使用できます。

TalkBack を有効にして、プレーヤーがゲームをプレイできることを確認しましょう。

  1. デバイスで TalkBack を有効にするには、こちらの手順を実施します。
  2. Unscramble アプリに戻ります。
  3. こちらの手順に沿って、TalkBack でアプリを探します。右にスワイプして画面要素間を順番に移動し、左にスワイプして逆に移動します。選択するには、任意の場所をダブルタップします。スワイプ操作でアプリのすべての要素にアクセスできることを確認します。
  4. TalkBack ユーザーが画面上の各アイテムに移動できることを確認します。
  5. TalkBack がスクランブルされた単語を通常の単語のように読み上げようとしていることがわかります。これは実際の単語ではないため、プレーヤーが混乱する可能性があります。
  6. ユーザー エクスペリエンスを改善する方法として、スクランブルされた単語の個々の文字を TalkBack で読み上げる方法があります。GameViewModel で、スクランブルされた単語 String を Spannable 文字列に変換します。Spannable 文字列とは、追加情報が付加された文字列です。ここでは、テキスト読み上げエンジンがスクランブルされた単語をそのまま一文字ずつ読み上げられるように、文字列を TYPE_VERBATIM の TtsSpan に関連付けます。
  7. 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 インスタンスを返すことができるようになります。

  1. Unscramble アプリを実行し、TalkBack でアプリを使用します。スクランブルされた単語の個々の文字が TalkBack で読み上げられるはずです。

アプリのユーザー補助機能を強化する方法について詳しくは、こちらの原則をご確認ください。

11. 使用しなくなったコードを削除する

解答コードで不要になったコードを削除するのは良いことです。これによりコードの管理が容易になり、新しいチームメンバーもコードを理解しやすくなります。

  1. GameFragment の getNextScrambledWord() メソッドと onDetach() メソッドを削除します。
  2. GameViewModel の onCleared() メソッドを削除します。
  3. ソースファイルの先頭にある使用されていないインポートを削除します。これはグレー表示されます。

ログ ステートメントも不要になりました。コードから削除しても構いません。

  1. (省略可)ViewModel のライフサイクルを理解するために前の Codelab でソースファイル(GameFragment.kt と GameViewModel.kt)に追加した Log ステートメントを削除します。

12. 解答コード

この Codelab の解答コードは、以下に示すプロジェクトにあります。

  1. プロジェクト用に提供されている GitHub リポジトリ ページに移動します。
  2. ブランチ名が Codelab で指定されたブランチ名と一致していることを確認します。たとえば、次のスクリーンショットでは、ブランチ名は main です。

1e4c0d2c081a8fd2.png

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

1debcf330fd04c7b.png

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

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで、[Open] をクリックします。

d8e9dbdeafe9038a.png

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

8d1fda7396afe8e5.png

  1. ファイル ブラウザで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開かれるまで待ちます。
  4. 実行ボタン 8de56cba7583251f.png をクリックして、アプリをビルドし、実行します。期待どおりにビルドされることを確認します。

13. まとめ

  • LiveData にはデータが保持されます。LiveData はあらゆる種類のデータに使用できるラッパーです。
  • LiveData は監視可能です。つまり、LiveData オブジェクトに保持されているデータが変更されるとオブザーバーに通知されます。
  • LiveData はライフサイクルに対応しています。オブザーバーを LiveData に接続すると、オブザーバーは LifecycleOwner(通常はアクティビティまたはフラグメント)に関連付けられます。LiveData により、ライフサイクルの状態がアクティブ(STARTED や RESUMED など)なオブザーバーのみが更新されます。LiveData と監視について詳しくは、こちらをご覧ください。
  • アプリでは、データ バインディングとバインディング式を使用してレイアウトからの LiveData の変更をリッスンできます。
  • バインディング式は、レイアウト内のレイアウト プロパティを参照する属性プロパティ(android:text など)で記述されます。

14. 関連リンク

ブログ投稿