知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day16【LiveData and LiveData observers編】
学ぶこと
LiveData
オブジェクトが役立つ理由ViewModel
に保存されているデータにLiveData
を追加する方法MutableLiveData
をいつどのように使用するかLiveData
の変更を監視するための observer method を追加する方法- バッキングプロパティを使用して
LiveData
をカプセル化する方法 - UI controller とそれに対応する
ViewModel
の間で通信する方法
すること
- GuessTheWord アプリの単語とスコアに
LiveData
を使用する - 単語またはスコアの変更を通知する observer を追加します
- 変更された値を表示する text view を更新します
LiveData
オブザーバーパターンを使用して、ゲーム終了イベントを追加します- Play Again ボタンを実装します
アプリの概要
このコードラボでは、ユーザーがアプリ内の全ての単語を循環した時にゲームを終了するイベントを追加することで、GuessTheWord アプリを改善します。また、score fragment に Play Aganin ボタンを追加して、ユーザがゲームを再度プレイできるようにします。
LiveData を GameViewModel に追加します
LiveData
は、ライフサイクルを意識した observable なデータホルダークラスです。例えば、GuessTheWord アプリで現在のスコアの周りに LiveData
をラップできます。このコードラボでは、LiveData
のいくつかの特性について学習します:
LiveData
は observable です。つまり、LiveData
オブジェクトが保持するデータが変更されると、オブザーバーに通知されます。LiveData
はデータを保持します。LiveData
は、任意のデータを使用できるラッパーです。LiveData
はライフサイクルに対応しています。observer をLiveData
にアタッチすると、オブザーバーはLifecycleOwner
(通常は Activity or Fragment) に関連付けられます。LiveData
は、STARTED
やRESUMED
などの Active なライフサイクル状態にあるオブザーバーのみを更新します。LiveData
と監視について詳しくは、こちら をご覧ください。
このタスクでは、GameViewModel
の現在のスコアと現在の単語データを LiveData
に変換することにより、任意のデータ型を LiveData
オブジェクトにラップする方法を学習します。後のタスクで、これらの LiveData
オブジェクトにオブザーバーを追加し、LiveData
を監視する方法を学習します。
LiveData を使用するようにスコアと単語を変更します
screens/game
パッケージの下で、GameVieModel
ファイルを開きます。score
とword
のタイプをMutableLiveData
に変更します。
MutableLiveData
は、値を変更できる LiveData
です。MutableLiveData
はジェネリッククラスであるため、保持するデータのタイプを指定する必要があります。
// The current word val word = MutableLiveData<String>() // The current score val score = MutableLiveData<Int>()
GameViewModel
のinit
ブロック内で、score
とword
を初期化します。LiveData
変数の値を変更するには、変数でsetValue()
メソッドを使用します。Kotlin では、value
プロパティを使用してsetValue()
を呼び出すことができます。
init { word.value = "" score.value = 0 ... }
LiveData オブジェクト参照を更新します
score
と word
変数は、LiveData
プロパティになりました。このステップでは、value
プロパティを使用して、これらの変数への参照を変更します。
GameViewModel
のonSkip()
メソッドないのscore
をscore.value
に変更します。score
がnull
である可能性があるというエラーに注意してください。次にこのエラーを修正します。エラーを解決するには、
onSkip()
のscore.value
にnull
チェックを追加します。次に、score
でminus()
関数を呼び出します。この関数は、null
で安全に減算を実行します。
fun onSkip() { score.value = (score.value)?.minus(1) nextWord() }
onCorrect()
メソッドも同様に更新します。score
でnull
チェックを追加、plus
関数を追加します。
fun onCorrect() { score.value = (score.value)?.plus(1) nextWord() }
GameViewModel
で、nextWord()
メソッド内で、単語参照をword.value
に変更します。
private fun nextWord() { if (!wordList.isEmpty()) { //Select and remove a word from the list word.value = wordList.removeAt(0) } }
GameFragment
のupdateWordText()
メソッド内で、viewModel.word
の参照をviewModel.word.value
に変更します。
private fun updateScoreText() { binding.scoreText.text = viewModel.score.value.toString() }
GameFragment
のupdateScoreText()
メソッド内で、viewModel.score
の参照をviewModel.score.value
に変更します。
private fun updateScoreText() { binding.scoreText.text = viewModel.score.value.toString() }
GameFragment
のgameFinished()
メソッド内で、viewModel.score
の参照をviewModel.score.value
に変更します。必要なnull
安全性チェックを追加します。
private fun gameFinished() { Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show() val action = GameFragmentDirections.actionGameToScore() action.score = viewModel.score.value?:0 NavHostFragment.findNavController(this).navigate(action) }
- コードにエラーがないことを確認してください。アプリをコンパイルして実行します。アプリの機能は以前と同じである必要があります。
Onservers に LiveData オブジェクトをアタッチする
このタスクは、スコアと単語のデータを LiveData
オブジェクトに変換した前のタスクと密接に関連しています。このタスクでは、LiveData
オブジェクトを Onserver
オブジェクトにアタッチします。Fragment view(viewLifecycleOwner) を LifecycleOwner
として使用します。
なぜ viewLifecycleOwner を使用するのですか?
Fragment 自体が破棄されていなくても、ユーザーが Fragment から移動すると、Fragment view は破棄されます。これにより、基本的に、Fragment のライフサイクルと Fragment の view のライフサイクルが2つのライフサイクルが作成されます。Fragment view のライフサイクルではなく Fragment のライフサイクルを参照すると、Fragment の view を更新する時に微妙なバグが発生する可能性があります。したがって、Fragment view に影響を与える observer を設定する時は、次のことを行う必要があります:
onCreateView()
で observers をセットアップしますviewLifecycleOwner
をオブザーバーに渡しますGameFragment
のonCreateView()
メソッド内で、現在のスコアviewModel.score
のLiveData
オブジェクトにObserver
オブジェクトをアタッチします。observe()
メソッドを使用し、viewModel
の初期化後にコードを配置します。ラムダ式を使用してコードを単純化します。(ラムダ式は、宣言されていない無名関数ですが、式としてすぐに渡されます。)
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
})
Observer
への参照を解決します。これを行うには、Observer
をクリックし、Option + Enter
を押して、androidx.lifecycle.Observer
をインポートします。
- 作成したばかりの observer は、監視対象の
LiveData
オブジェクトが保持するデータが変更された時にイベントを受信します。observer 内で、scoreTextView
をnewScore
で更新します。
/** Setting up LiveData observation relationship **/ viewModel.score.observe(viewLifecycleOwner, Observer { newScore -> binding.scoreText.text = newScore.toString() })
Observer
オブジェクトを現在の単語LiveData
オブジェクトにアタッチします。Observer
オブジェクトを現在のスコアにアタッチしたのと同じ方法で行います。
/** Setting up LiveData observation relationship **/ viewModel.word.observe(viewLifecycleOwner, Observer { newWord -> binding.wordText.text = newWord })
score
または word
の値が変更されると、画面に表示される score
または word
が自動的に更新されるようになりました。
GameFragment
で、updateWordText()
とupdateScoreText()
、およびそれらへの全ての参照を削除します。text view はLiveData
observer method によって更新されるため、これらはもう必要ありません。アプリを実行します。ゲームアプリは以前と全く同じように機能するはずですが、現在は
LiveData
およびLiveData
observers を使用しています。
LiveData をカプセル化する
カプセル化は、オブジェクトの一部のフィールドへの直接アクセスを制限する方法です。オブジェクトをカプセル化する時は、プライベート内部フィールドを変更する一連のパブリックメソッドを公開します。カプセル化を使用して、他のクラスがこれらの内部フィールドを操作する方法を制御します。
現在のコードでは、外部クラスは、value
プロパティを使用して、score
と word
変数を変更できます。このコードラボで開発しているアプリでは問題にならないかもしれませんが、本番アプリでは、ViewModel
オブジェクトのデータを制御する必要があります。
ViewModel
のみがアプリのデータを編集する必要があります。ただし。UI controller はデータを読み取る必要があるため、データフィールドを完全にプライベートにすることはできません。アプリのデータをカプセル化するには、ModuleLiveData
オブジェクトと LiveData
オブジェクトの両方を使用します。
MutableLiveData
vs LiveData
:
- 名前が示すように、
MutableLiveData
オブジェクトのデータは変更できます。ViewModel
内では、データを編集可能である必要があるため、MutableLiveData
を使用します。 LiveData
オブジェクトのデータは読み取ることができますが、変更することはできません。ViewModel
の外部からは、データは読み取り可能である必要がありますが、編集可能ではないため、データはLiveData
として公開する必要があります。
この戦力を実行するには、Kotlin backing property を使用します。backing property を使用すると、正確なオブジェクト以外のゲッターから何かを返すことができます。このタスクでは、GuessTheWord アプリで score
オブジェクトと word
オブジェクトの backing property を実装します。
score と word に backing property を追加する
GameViewModel
で、現在のscore
オブジェクトをprivate
にします。backing property で使用される命名規則に従うには、
score
を_score
に変更します。_score
プロパティは、内部で使用されるゲームスコアの可変バージョンになりました。score
と呼ばれるLiveData
タイプのパブリックバージョンを作成します。
// The current score private val _score = MutableLiveData<Int>() val score: LiveData<Int>
- 初期化エラーが表示されます。このエラーは、
GameFragment
内で、score
がLiveData
参照であり、スコアがその setter にアクセスできなくなったために発生します。Kotlin の getter と setter の詳細については、Getters and Setters を参照してください。
エラーを解決するには、GameViewModel
の score
オブジェクトの get()
メソッドをオーバーライドし、backing property _score
を返します。
val score: LiveData<Int> get() = _score
GameViewModel
で、スコアの参照を内部の可変バージョン_score
に変更します。
init { ... _score.value = 0 ... } ... fun onSkip() { _score.value = (score.value)?.minus(1) ... } fun onCorrect() { _score.value = (score.value)?.plus(1) ... }
score
オブジェクトの場合と同様に、word
オブジェクトの名前を_word
に変更し、その backing property を追加します。
// The current word private val _word = MutableLiveData<String>() val word: LiveData<String> get() = _word ... init { _word.value = "" ... } ... private fun nextWord() { if (!wordList.isEmpty()) { //Select and remove a word from the list _word.value = wordList.removeAt(0) } }
game-finished イベント追加する
ユーザが End Game ボタンをタップすると、現在のアプリはスコア画面に移動します。また、プレーヤーが全ての単語を循環した時に、アプリがスコア画面に移動するようにします。プレーヤーが最後の単語を終了した後、ユーザがボタンをタップする必要が内容に、ゲームを自動的に終了する必要があります。
この機能を実装するには、全ての単語が表示された時にイベントがトリガーされ、ViewModel
から Fragment に伝達される必要があります。これを行うには、LiveData
オブザーバーパターンを使用して、ゲームが終了したイベントをモデル化します。
Observer pattern
observer pattern は software design pattern です。オブジェクト間の通信を指定します: observable(observable の "subject") と observers。observable は、その状態の変化について observer に通知するオブジェクトです。
このアプリの LiveData
の場合、Observable("subject") は LiveData
オブジェクトであり、obsrvers は Fragment などの UI controllers です。状態の変更は、LiveData
内にラップされたデータが変更されるたびに発生します。LiveData
クラスは、ViewModel
から Fragment への通信に不可欠です。
LiveData を使用して、ゲームが終了したイベントを検出します
このタスクでは、LiveData
オブザーバーパターンを使用して、ゲームが終了したイベントをモデル化します。
GameViewModel
で、_eventGameFinish
というBoolean
のMutableLiveData
オブジェクトを作成します。このオブジェクトは、ゲーム終了イベントを保持します。_eventGameFinish
オブジェクトを初期化した後、eventGameFinish
という backing property を作成して初期化します。
// Event which triggers the end of the game private val _eventGameFinish = MutableLiveData<Boolean>() val eventGameFinish: LiveData<Boolean> get() = _eventGameFinish
GameViewMode
で、onGameFinish()
メソッドを追加します。このメソッドで、ゲーム終了イベントeventGameFinish
をtrue
に設定します。
/** Method for the game completed event **/ fun onGameFinish() { _eventGameFinish.value = true }
GameViewModel
のnextWord()
メソッド内で、単語リストが空の場合はゲームを終了します。
private fun nextWord() { if (wordList.isEmpty()) { onGameFinish() } else { //Select and remove a _word from the list _word.value = wordList.removeAt(0) } }
GameFragment
のonCreateView()
内で、viewModel
を初期化した後、オブザーバーをeventGameFinish
にアタッチします。onserver()
) メソッドを使用します。ラムダ関数内で、gameFinished()
メソッドを呼び出します。
// Observer for the Game finished event viewModel.eventGameFinish.observe(viewLifecycleOwner, Observer<Boolean> { hasFinished -> if (hasFinished) gameFinished() })
- アプリを実行し、ゲームをプレイして、全ての単語を確認します。アプリは End Game をタップするまでゲームの断片にとどまるのではなく、自動的にスコア画面に移動します。
単語リストが空になった後、eventGameFinish
が設定され、game fragment 内の関連するオブザーバーメソッドが呼び出され
アプリは画面 Fragment に移動します。
- 追加したコードにより、ライフサイクルの問題が発生しました。この問題を理解するには、
GameFragment
クラスで、gameFinished()
メソッドの navigation コードをコメントアウトします。Toast
メッセージを保持するようにしてください。
private fun gameFinished() { Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show() // val action = GameFragmentDirections.actionGameToScore() // action.score = viewModel.score.value?:0 // NavHostFragment.findNavController(this).navigate(action) }
- アプリを実行し、ゲームをプレイして、全ての words を確認します。toast メッセージがゲーム画面の下部に短時間表示されます。これは予想される動作です。
次に、デバイスまたはエミュレータを回転させます。Toast がまた表示されます。これはバグです。ゲームが終了すると、Toast は一回だけ表示されるはずだからです。fragment が再作成されるたびに Toast が表示されるわけではありません。この問題は次のタスクで解決します。
ゲーム終了イベントをリセットする
通常、LiveData
は、データが更新された場合にのみ observer に更新を配信します。この動作の例外は、observer は非アクティブ状態からアクティブ状態に変化した時にも更新を受信することです。
これが、ゲーム終了の Toast がアプリで繰り返しトリガーされる理由です。画面の回転後に game fragment が再作成されると、game fragment は非アクティブ状態からアクティブ状態に移行します。fragment 内の observer は既存の ViewModel
に再接続され、現在のデータを受信します。gameFinished()
メソッドが再トリガーされ、Toast が表示されます。
このタスクでは、GameViewModel
の eventGameFinish
フラグをリセットすることにより、この問題を修正し、Toast を一回だけ表示します。
GameViewModel
で、onGameFinishComplete()
メソッドを追加して、ゲーム終了イベント_eventGameFinish
をリセットします。
/** Method for the game completed event **/ fun onGameFinishComplete() { _eventGameFinish.value = false }
GameFragment
で、gameFinished()
の最後に、viewModel
オブジェクトでonGameFinishComplete()
を呼び出します。今のところ、gameFinished()
のナビゲーションコードはコメントアウトしたままにしておきます。
private fun gameFinished() { ... viewModel.onGameFinishComplete() }
アプリを実行してゲームをプレイします。全ての単語をパスし、デバイスの画面の向きを変更します。Toast は一回だけ表示されます。
GameFragment
のgameFinished()
メソッド内で、navigation code のコメントを解除します。
private fun gameFinished() { Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show() val action = GameFragmentDirections.actionGameToScore() action.score = viewModel.score.value?:0 findNavController(this).navigate(action) viewModel.onGameFinishComplete() }
Android Studio からプロンプトが表示されたら、androidx.navigation.fragment.NavHostFragment.findNavController()
をインポートします。
- アプリを実行してゲームをプレイします。全ての単語を読み終えたら、アプリが自動的に最終スコア画面に移動することを確認してください。
LiveData を ScoreViewModel に追加します
このタスクでは、ScoreViewMode
でスコアを LiveData
オブジェクトに変更し、それに observer をアタッチします。このタスクは、LiveData
を GameViewModel
に追加した時に行ったことと似ています。
完全を期すために ScoreViewModel
にこれらの変更を加えて、アプリ内の全てのデータが LiveData
を使用するようにします。
ScoreViewModel
で、スコア変数タイプをMutableLiveData
に変更します。慣例により名前を_score
に変更し、backing property を追加します。
private val _score = MutableLiveData<Int>() val score: LiveData<Int> get() = _score
ScoreViewModel
のinit
ブロック内で、_score
を初期化します。必要に応じて、ログを削除するか、init ブロックに残すかを選択します。
init {
_score.value = finalScore
}
ScoreFragment
のonCreateView()
内で、viewModel
を初期化した後、スコアLiveData
オブジェクトの observer をアタッチします。ラムダ式内で、スコア値を score text view に直接割り当てるコードをViewModel
から削除します。
Code to add:
// Add observer for score viewModel.score.observe(viewLifecycleOwner, Observer { newScore -> binding.scoreText.text = newScore.toString() })
Code to remove:
binding.scoreText.text = viewModel.score.toString()
Android Studio から プロンプトが表示されたら、androidx.lifycycle.Observer
をインポートします。
- アプリを実行してゲームをプレイします。アプリは以前と同じように機能するはずですが、現在は
LiveData
とオブザーバーを使用してスコアを更新しています。
Play Again button を追加する
このタスクでは、スコア画面に Play Again ボタンを追加し、LiveData
イベントを使用してクリックリスナーを実装します。ボタンは、スコア画面からゲーム画面に移動するイベントをトリガーします。
アプリのスターターコードには Play Again ボタンが含まれていますが、ボタンは非表示になっています。
res/layout/score_fragment.xml
で、play_again_button
ボタンの場合、visibility
attributes の値をvisible
に変更します。
<Button android:id="@+id/play_again_button" ... android:visibility="visible" />
ScoreViewModel
で、_eventPlayAgain
というBoolean
を保持するLiveData
オブジェクトを追加します。このオブジェクトは、LiveData
イベントを保存して、スコア画面からゲーム画面に移動するために使用されます。
private val _eventPlayAgain = MutableLiveData<Boolean>() val eventPlayAgain: LiveData<Boolean> get() = _eventPlayAgain
ScoreViewModel
で、イベント_eventPlayAgain
を設定およびリセットするメソッドを定義します。
fun onPlayAgain() { _eventPlayAgain.value = true } fun onPlayAgainComplete() { _eventPlayAgain.value = false }
ScoreFragment
で、eventPlayAgain
のオブザーバーを追加します。onCreateView()
の最後、return ステートメントの前にコードを配置します。ラムダ式内で、ゲーム画面に戻り、eventPlayAgain
をリセットします。
// Navigates back to game when button is pressed viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer { playAgain -> if (playAgain) { findNavController().navigate(ScoreFragmentDirections.actionRestart()) viewModel.onPlayAgainComplete() } })
Android Studio のプロンプトが表示されたら、androidx.navigation.fragment.findNavController
をインポートします。
ScoreFragment
のonCreateView()
内で、クリックリスナーを Play Again ボタンに追加し、viewModel.onPlayAgain()
を呼び出します。
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }
- アプリを実行してゲームをプレイします。ゲームを終了すると、スコア画面に最終スコアと Play Again ボタンが表示されます。Play Again ボタンをタップすると、アプリがゲーム画面に移動し、ゲームを再開できます。
まとめ
LiveData
LiveData
は、Android Architecture Components の1つである、ライフサイクル対応の observable なデータホルダークラスです。LiveData
を使用して、データが更新された時に UI が自動的に更新されるようにすることができます。LiveData
は observable です。つまり、LiveData
オブジェクトが保持するデータが変更された時に、Activity や Fragment などの observer に通知できます。LiveData
はデータを保持します。これは、任意のデータで使用できるラッパーです。LiveData
はライフサイクルに対応しています。つまり。STARTED
やRESUMED
などのアクティブなライフサイクル状態にある observer のみを更新します。
LiveData を追加する
ViewModel
のデータ変数のタイプをLiveData
またはMutableLiveData
に変更します。
MutableLiveData
は、値を変更できる LiveData
オブジェクトです。MutableLiveData
はジェネリッククラスであるため、保持するデータのタイプを指定する必要があります。
LiveData
が保持するデータの値を変更するには、LiveData
変数でsetValue()
メソッドを使用します。
LiveData をカプセル化する
ViewModel
内のLiveData
は編集可能である必要があります。ViewModel
の外部では、LiveData
が読み取り可能である必要があります。これは、Kotlin backing property を使用して実装できます。- Kotlin backing property を使用すると、正確なオブジェクト以外の何かをゲッターから返すことができます。
LiveData
をカプセル化するには、ViewModel
内でprivate
MutableLiveData
を使用し、ViewModel
外でLiveData
backing property を返します。
Observable LiveData
LiveData
はオブザーバーパターンに従います。"Observable" はLiveData
オブジェクトであり、オブザーバーは fragment のような UI controller のメソッドです。LiveData
内にラップされたデータが変更されるたびに、UI controller オブザーバーメソッドに通知されます。LiveData
を observable にするには、observe()
) メソッドを使用して、オブザーバーオブジェクト(activity や fragment) をオブザーバーのLiveData
参照にアタッチします。LiveData
オブザーバーパターンは、ViewModel
から、UI controller への通信に使用できます。