Android Kotlin基礎講座 05.1: ViewModelとViewModelProvider
目次
タスク:GameViewModelを導入する
ViewModelはコンフィグレーション変化の前後でも生き残るので、コンフィグレーション変化に対応させたいデータを置く場所として適しています。
- 画面に表示されるデータ、およびそのデータを処理するためのコードをViewModelの中に置いてください。
- ViewModelにはフラグメント、アクティビティ、またはビューなどの参照を含ませるべきではありません。なぜならそれらはコンフィグレーション変化の後に生き残らないからです。
比較のために、ここではViewModelが追加される前と後でどのようにGameFragmentのUIデータが扱われるかを説明しています。
- ViewModelを追加する前:
画面回転のようなコンフィグレーション変化が起こると、ゲームフラグメントは破棄され、再生成されます。データは失われます。 - ViewModelを追加しゲームフラグメントのUIデータをViewModelに移した後:
フラグメントが表示するために必要なすべてのデータはViewModelの中にあります。コンフィグレーション変化が起こってもViewModelは生存し続けるため、そこに含まれるデータも保持されます。
このタスクでは、アプリのUIデータとデータを処理するためのメソッドをGameViewModelクラスに移行します。これをすることによってコンフィグレーション変化の後でもデータが保持されます。
ステップ1:データフィールドとデータ処理をViewModelに移行する
データフィールドとメソッドをGameFragmentからGameViewModelに移行しましょう。
- word、score、wordListデータフィールドを移行してください。wordとscoreがprivateになっていないことを確認してください。
GameFragmentBindingというバインディング変数は移さないでください。これにはビューへの参照が含まれているためです。この変数はフラグメントの役割であるレイアウトをインフレートするため、クリックリスナーをセットアップするため、そしてデータを画面上に表示するために使われます。 - reseList()メソッドとnextWord()メソッドを移行してください。これらのメソッドは画面上にどの単語を表示するかを決定します。
- onCreateView()メソッドの中のresetList()とnextWord()メソッドの呼び出しをGameViewModelのinitブロックの中に移行してください。
これらのメソッドはinitブロックの中に置くべきです。なぜならViewModelが生成されたときに単語リストをリセットするべきだからです。フラグメントが作られる度にするべきではありません。GameFragmentのinitブロックの中のログ文は削除して構いません。
GameFragmentのonSkip()とonCrrect()クリックハンドラーにはUIデータを処理し更新するためのコードが含まれています。このUIを更新するためにコードはフラグメントの中にあるべきですが、データを処理するためのコードはViewModelに移行されなければなりません。
ここではひとまず、同一のメソッドを両方に置きます。
- onSkip()メソッドとonCorrect()メソッドをコピーしてGameViewModelに貼り付けてください。
- GameViewModel中のonSkip()メソッドとonCorrect()メソッドがprivateでないようにしてください。フラグメントからこれらのメソッドへ参照するためです。
ここまででGameViewModelクラスは以下のようになります。
class GameViewModel : ViewModel() {
// The current word
var word = ""
// The current score
var score = 0
// The list of words - the front of the list is the next word to guess
private lateinit var wordList: MutableList<String>
/**
* Resets the list of words and randomizes the order
*/
private fun resetList() {
wordList = mutableListOf(
"queen",
"hospital",
"basketball",
"cat",
"change",
"snail",
"soup",
"calendar",
"sad",
"desk",
"guitar",
"home",
"railway",
"zebra",
"jelly",
"car",
"crow",
"trade",
"bag",
"roll",
"bubble"
)
wordList.shuffle()
}
init {
resetList()
nextWord()
Log.i("GameViewModel", "GameViewModel created!")
}
/**
* Moves to the next word in the list
*/
private fun nextWord() {
if (!wordList.isEmpty()) {
//Select and remove a word from the list
word = wordList.removeAt(0)
}
updateWordText()
updateScoreText()
}
/** Methods for buttons presses **/
fun onSkip() {
score--
nextWord()
}
fun onCorrect() {
score++
nextWord()
}
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
}
以下はGameFragmentクラスのコードです。
/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {
private lateinit var binding: GameFragmentBinding
private lateinit var viewModel: GameViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate view and obtain an instance of the binding class
binding = DataBindingUtil.inflate(
inflater,
R.layout.game_fragment,
container,
false
)
Log.i("GameFragment", "Called ViewModelProvider.get")
viewModel = ViewModelProvider(this).get(GameViewModel::class.java)
binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
updateScoreText()
updateWordText()
return binding.root
}
/** Methods for button click handlers **/
private fun onSkip() {
score--
nextWord()
}
private fun onCorrect() {
score++
nextWord()
}
/** Methods for updating the UI **/
private fun updateWordText() {
binding.wordText.text = word
}
private fun updateScoreText() {
binding.scoreText.text = score.toString()
}
}
ステップ2:GameFragmentのクリックハンドラーとデータフィールドへの参照を更新する
- GameFragmentの中のonSkip()メソッドとonCorrect()メソッドを更新します。得点を更新するコードを削除し、代わりに対応するviewModelのonSkip()メソッドとoncorrect()メソッドを呼び出してください。
- nextWord()メソッドはViewModelに移行したため、GameFragmentからはもうそれにアクセスできません。
GameFragmentのonSkip()メソッドとonCorrect()メソッドの中のnextWord()の呼び出しをupdateScoreText()とupdateWo
private fun onSkip() {
viewModel.onSkip()
updateWordText()
updateScoreText()
}
private fun onCorrect() {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}
- GameFragmentのscore変数とword変数をGameViewModel変数を使うように更新してください。それらの変数はGameViewModelにあるためです。
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
リマインダー: アプリのアクティビティ、フラグメント、ビューはコンフィグレーション変化のあとには保持されないので、ViewModelにはそれらに対する参照は含まれるべきではありません。
- GameViewModelのnextWord()メソッドの中のupdateWordText()メソッドとupdateScoreText()メソッドの呼び出しを削除してください。これらのメソッドは現在GameFragmentから呼び出されています。
- アプリをビルドしてエラーがないことを確認してください。もしエラーが発生した場合、プロジェクトをクリーニングしてリビルドしてください。
- アプリを起動し、いくつかの単語でゲームをプレイしてください。ゲーム画面にいる間に画面を回転させてください。現在の得点と単語がコンフィグレーション変化の後も保持されていることを確認してください。
これでアプリのデータはViewModel内に保存されるようになりました。コンフィグレーション変化の後でもデータが引き継がれます。