Android Kotlin基礎講座 05.1: ViewModelとViewModelProvider

Android Kotlin基礎講座

タスク:GameViewModelを導入する

ViewModelはコンフィグレーション変化の前後でも生き残るので、コンフィグレーション変化に対応させたいデータを置く場所として適しています。

  • 画面に表示されるデータ、およびそのデータを処理するためのコードをViewModelの中に置いてください。
  • ViewModelにはフラグメント、アクティビティ、またはビューなどの参照を含ませるべきではありません。なぜならそれらはコンフィグレーション変化の後に生き残らないからです。

比較のために、ここではViewModelが追加される前と後でどのようにGameFragmentのUIデータが扱われるかを説明しています。

  • ViewModelを追加する前:
    画面回転のようなコンフィグレーション変化が起こると、ゲームフラグメントは破棄され、再生成されます。データは失われます。
  • ViewModelを追加しゲームフラグメントのUIデータをViewModelに移した後:
    フラグメントが表示するために必要なすべてのデータはViewModelの中にあります。コンフィグレーション変化が起こってもViewModelは生存し続けるため、そこに含まれるデータも保持されます。

このタスクでは、アプリのUIデータとデータを処理するためのメソッドをGameViewModelクラスに移行します。これをすることによってコンフィグレーション変化の後でもデータが保持されます。

ステップ1:データフィールドとデータ処理をViewModelに移行する

データフィールドとメソッドをGameFragmentからGameViewModelに移行しましょう。

  1. word、score、wordListデータフィールドを移行してください。wordとscoreがprivateになっていないことを確認してください。

    GameFragmentBindingというバインディング変数は移さないでください。これにはビューへの参照が含まれているためです。この変数はフラグメントの役割であるレイアウトをインフレートするため、クリックリスナーをセットアップするため、そしてデータを画面上に表示するために使われます。
  2. reseList()メソッドとnextWord()メソッドを移行してください。これらのメソッドは画面上にどの単語を表示するかを決定します。
  3. onCreateView()メソッドの中のresetList()とnextWord()メソッドの呼び出しをGameViewModelのinitブロックの中に移行してください。

    これらのメソッドはinitブロックの中に置くべきです。なぜならViewModelが生成されたときに単語リストをリセットするべきだからです。フラグメントが作られる度にするべきではありません。GameFragmentのinitブロックの中のログ文は削除して構いません。

GameFragmentのonSkip()とonCrrect()クリックハンドラーにはUIデータを処理し更新するためのコードが含まれています。このUIを更新するためにコードはフラグメントの中にあるべきですが、データを処理するためのコードはViewModelに移行されなければなりません。

ここではひとまず、同一のメソッドを両方に置きます。

  1. onSkip()メソッドとonCorrect()メソッドをコピーしてGameViewModelに貼り付けてください。
  2. 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のクリックハンドラーとデータフィールドへの参照を更新する

  1. GameFragmentの中のonSkip()メソッドとonCorrect()メソッドを更新します。得点を更新するコードを削除し、代わりに対応するviewModelのonSkip()メソッドとoncorrect()メソッドを呼び出してください。
  2. nextWord()メソッドはViewModelに移行したため、GameFragmentからはもうそれにアクセスできません。

    GameFragmentのonSkip()メソッドとonCorrect()メソッドの中のnextWord()の呼び出しをupdateScoreText()とupdateWo
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. GameFragmentのscore変数とword変数をGameViewModel変数を使うように更新してください。それらの変数はGameViewModelにあるためです。
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}

リマインダー: アプリのアクティビティ、フラグメント、ビューはコンフィグレーション変化のあとには保持されないので、ViewModelにはそれらに対する参照は含まれるべきではありません。

  1. GameViewModelのnextWord()メソッドの中のupdateWordText()メソッドとupdateScoreText()メソッドの呼び出しを削除してください。これらのメソッドは現在GameFragmentから呼び出されています。
  2. アプリをビルドしてエラーがないことを確認してください。もしエラーが発生した場合、プロジェクトをクリーニングしてリビルドしてください。
  3. アプリを起動し、いくつかの単語でゲームをプレイしてください。ゲーム画面にいる間に画面を回転させてください。現在の得点と単語がコンフィグレーション変化の後も保持されていることを確認してください。

これでアプリのデータはViewModel内に保存されるようになりました。コンフィグレーション変化の後でもデータが引き継がれます。

コメント

プロフィール

プロフィール
コードラボJP

大学卒業後SEに就職、現在は退職しフリーランスとして活動中。
『初心者でも挫折せずに一人でプログラミングを学べる』をモットーに、コードラボJPを開設
お問い合わせ等はcodelabsjp@gmail.comまで

コードラボJPをフォローする
タイトルとURLをコピーしました