Android Kotlin の基礎 05.1: ViewModel と ViewModelFactory

この Codelab は、Android Kotlin の基礎コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。コースのすべての Codelab は、Android Kotlin の基礎の Codelab のランディング ページに一覧表示されています。

タイトル画面

ゲーム画面

スコア画面

はじめに

この Codelab では、Android アーキテクチャ コンポーネントの 1 つである ViewModel について学習します。

  • ViewModel クラスを使用して、ライフサイクルを意識した方法で UI 関連のデータを保存し管理します。ViewModel クラスを使用すると、画面の回転やキーボードの利用可能性の変更などのデバイス構成の変更後もデータを維持することができます。
  • ViewModelFactory クラスを使用して、構成の変更後も存続する ViewModel オブジェクトをインスタンス化して返します。

前提となる知識

  • Kotlin で基本的な Android アプリを作成する方法。
  • ナビゲーション グラフを使用してアプリにナビゲーションを実装する方法。
  • アプリのデスティネーション間を移動し、ナビゲーション デスティネーション間でデータを渡すコードを追加する方法。
  • アクティビティとフラグメントのライフサイクルの仕組み。
  • アプリにログ情報を追加し、Android Studio の Logcat を使用してログを読み取れること

学習内容

演習内容

  • アプリに ViewModel を追加して、構成変更後もデータが維持されるようにアプリのデータを保存します。
  • ViewModelFactory とファクトリー メソッド デザイン パターンを使用して、コンストラクタ パラメータを含む ViewModel オブジェクトをインスタンス化します。

レッスン 5 の Codelab では、スターター コードから GuessTheWord アプリを開発します。GuessTheWord は、2 人のプレーヤーが最高スコアを目指して協力する、2 プレーヤー ジェスチャー ゲームです。

最初のプレーヤーはアプリの単語を見て、2 番目のプレーヤーに単語を見せないようにしながら、各単語を順番に演じます。2 人目のプレーヤーが単語を当てようとします。

ゲームをプレイするには、最初のプレーヤーがデバイスでアプリを開き、下のスクリーンショットに示すように、「ギター」などの単語を表示します。

最初のプレーヤーは、単語自体を言わないように注意しながら、単語を演じます。

  • 2 人目のプレーヤーが単語を正しく当てると、1 人目のプレーヤーが [Got It] ボタンを押します。これにより、カウントが 1 つ増え、次の単語が表示されます。
  • 2 人目のプレーヤーが単語を当てられない場合、1 人目のプレーヤーが [スキップ] ボタンを押します。これにより、カウントが 1 減り、次の単語にスキップします。
  • ゲームを終了するには、[End Game] ボタンを押します。(この機能は、シリーズの最初の Codelab のスターター コードには含まれていません)。

このタスクでは、スターター アプリをダウンロードして実行し、コードを確認します。

ステップ 1: 開始する

  1. GuessTheWord スターター コードをダウンロードして、Android Studio でプロジェクトを開きます。
  2. Android 搭載デバイスまたはエミュレータでアプリを実行します。
  3. ボタンをタップします。[Skip] ボタンをタップすると、次の単語が表示され、スコアが 1 減ります。[Got It] ボタンをタップすると、次の単語が表示され、スコアが 1 増えます。[End Game] ボタンは実装されていないため、タップしても何も起こりません。

ステップ 2: コードのウォークスルーを行う

  1. Android Studio でコードを確認して、アプリの動作を把握します。
  2. 特に重要な以下のファイルを確認してください。

MainActivity.kt

このファイルには、デフォルトのテンプレート生成コードのみが含まれています。

res/layout/main_activity.xml

このファイルには、アプリのメイン レイアウトが含まれています。NavHostFragment は、ユーザーがアプリ内を移動する際に他のフラグメントをホストします。

UI フラグメント

スターター コードには、com.example.android.guesstheword.screens パッケージの下の 3 つの異なるパッケージに 3 つのフラグメントがあります。

  • title/TitleFragment: タイトル画面
  • ゲーム画面の game/GameFragment
  • スコア画面の score/ScoreFragment

screens/title/TitleFragment.kt

タイトル フラグメントは、アプリの起動時に最初に表示される画面です。クリック ハンドラは、ゲーム画面に移動するために [Play] ボタンに設定されています。

screens/game/GameFragment.kt

メイン フラグメントであり、ゲームのアクションのほとんどはここで発生します。

  • 現在の単語と現在のスコアの変数が定義されています。
  • resetList() メソッド内で定義された wordList は、ゲームで使用される単語のサンプル リストです。
  • onSkip() メソッドは、[スキップ] ボタンのクリック ハンドラです。スコアを 1 減らし、nextWord() メソッドを使用して次の単語を表示します。
  • onCorrect() メソッドは、[Got It] ボタンのクリック ハンドラです。このメソッドは、onSkip() メソッドと同様に実装されます。唯一の違いは、このメソッドではスコアから減算するのではなく、1 を加算することです。

screens/score/ScoreFragment.kt

ScoreFragment はゲームの最終画面で、プレーヤーの最終スコアが表示されます。この Codelab では、この画面を表示して最終スコアを表示する実装を追加します。

res/navigation/main_navigation.xml

ナビゲーション グラフは、ナビゲーションを介してフラグメントがどのように接続されているかを示しています。

  • タイトル フラグメントから、ゲーム フラグメントに移動できます。
  • ゲーム フラグメントから、スコア フラグメントに移動できます。
  • スコア フラグメントから、ゲーム フラグメントに戻ることができます。

このタスクでは、GuessTheWord スターター アプリの問題を見つけます。

  1. スターター コードを実行し、数個の単語をプレイします。各単語の後に [Skip] または [Got It] をタップします。
  2. ゲーム画面に単語と現在のスコアが表示されるようになりました。デバイスまたはエミュレータを回転して、画面の向きを変更すると、現在のスコアが失われます。
  3. ゲームを数語プレイします。ゲーム画面にスコアが表示されたら、アプリを閉じて再び開きます。アプリの状態が保存されていないため、ゲームが最初から再開されることに注目してください。
  4. 数個の単語をプレイしてから、[End Game] ボタンをタップします。何も起こらないことを確認します。

アプリに関する問題:

  • スターター アプリでは、デバイスの画面の向きが変化したときや、アプリがシャットダウンして再起動したときなどの構成変更時に、アプリの状態が保存されません。
    この問題は、onSaveInstanceState() コールバックを使用して解決できます。ただし、onSaveInstanceState() メソッドを使用する場合、バンドルに状態を保存するためのコードや、その状態を取得するロジックを実装する必要があります。また、保存できるデータの量は最小限です。
  • ユーザーが [End Game] ボタンをタップしても、ゲーム画面がスコア画面に移動しません。

この問題を解決するには、この Codelab で学習するアプリ アーキテクチャ コンポーネントを使用します。

アプリ アーキテクチャ

アプリ アーキテクチャは、コードが整理され、特定のシナリオでパフォーマンスが向上し、操作が容易になるように、アプリのクラスとその関係を設計する方法です。この 4 つの Codelab では、GuessTheWord アプリに加える改善は Android アプリ アーキテクチャのガイドラインに沿っており、Android アーキテクチャ コンポーネントを使用します。Android アプリのアーキテクチャは、MVVM(モデル - ビュー - ビューモデル)アーキテクチャ パターンに似ています。

GuessTheWord アプリは、関心の分離という設計原則に従い、クラスに分割されています。各クラスは別々の関心事に対応しています。このレッスン最初の Codelab では、UI コントローラ、ViewModelViewModelFactory を使用します。

UI コントローラ

UI コントローラは、ActivityFragment などの UI ベースのクラスです。UI コントローラには、ビューの表示やユーザー入力の取得など、UI やオペレーティング システムとのやり取りを処理するロジックのみを含めるべきです。表示するテキストを決定するロジックなどの意思決定ロジックは、UI コントローラに入れないでください。

GuessTheWord スターター コードでは、UI コントローラは GameFragmentScoreFragment,TitleFragment の 3 つのフラグメントです。「関心の分離」という設計原則に従い、GameFragment はゲーム要素を画面に描画し、ユーザーがボタンをタップしたタイミングを把握するだけで、それ以上のことは行いません。ユーザーがボタンをタップすると、この情報が GameViewModel に渡されます。

ViewModel

ViewModel は、関連付けられたフラグメントまたはアクティビティに表示されるデータを保持します。ViewModelまた、UI コントローラがデータを表示するための前処理として、簡単な計算と変換を行えます。ViewModelこのアーキテクチャでは、ViewModel が意思決定を行います。

GameViewModel は、スコア値、単語のリスト、現在の単語など、画面に表示するデータを保持します。GameViewModel には、データの現在の状態を判断するための簡単な計算を行うビジネス ロジックも含まれています。

ViewModelFactory

ViewModelFactory は、コンストラクタ パラメータの有無にかかわらず、ViewModel オブジェクトをインスタンス化します。

後の Codelab では、UI コントローラと ViewModel に関連する他の Android アーキテクチャ コンポーネントについて学習します。

ViewModel クラスは、UI 関連のデータを保存し管理するように設計されています。このアプリでは、各 ViewModel は 1 つのフラグメントに関連付けられています。

このタスクでは、アプリに最初の ViewModelGameFragmentGameViewModel)を追加します。また、ViewModel がライフサイクルを認識するとはどういうことかも学びます。

ステップ 1: GameViewModel クラスを追加する

  1. build.gradle(module:app) ファイルを開きます。dependencies ブロック内で、ViewModel の Gradle 依存関係を追加します。

    ライブラリの最新バージョンを使用すると、ソリューション アプリは想定どおりにコンパイルされます。そうでない場合は、問題を解決するか、以下のバージョンに戻してみてください。
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. screens/game/ パッケージ フォルダに、GameViewModel という名前の新しい Kotlin クラスを作成します。
  2. GameViewModel クラスが抽象クラス ViewModel を拡張するようにします。
  3. ViewModel がライフサイクルを認識する方法を理解するために、log ステートメントを含む init ブロックを追加します。
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

ステップ 2: onCleared() をオーバーライドしてロギングを追加する

ViewModel は、関連付けられたフラグメントの接続が解除されたとき、またはアクティビティが終了したときに破棄されます。ViewModel が破棄される直前には、onCleared() コールバックが呼び出されてリソースがクリーンアップされます。

  1. GameViewModel クラスで、onCleared() メソッドをオーバーライドします。
  2. onCleared() 内にログ ステートメントを追加して、GameViewModel のライフサイクルを追跡します。
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

ステップ 3: GameViewModel をゲーム フラグメントに関連付ける

ViewModel は UI コントローラに関連付ける必要があります。この 2 つを関連付けるには、UI コントローラ内に ViewModel への参照を作成します。

このステップでは、対応する UI コントローラ(GameFragment)内に GameViewModel の参照を作成します。

  1. GameFragment クラスで、GameViewModel 型のフィールドをトップレベルにクラス変数として追加します。
private lateinit var viewModel: GameViewModel

ステップ 4: ViewModel を初期化する

画面の回転などの構成の変更中には、フラグメントなどの UI コントローラが再作成されます。ただし、ViewModel インスタンスは存続します。ViewModel クラスを使用して ViewModel インスタンスを作成すると、フラグメントが再作成されるたびに新しいオブジェクトが作成されます。代わりに、ViewModelProvider を使用して ViewModel インスタンスを作成します。

ViewModelProvider の仕組み:

  • ViewModelProvider は、既存の ViewModel が存在する場合はそれを返し、存在しない場合は新しい ViewModel を作成します。
  • ViewModelProvider は、指定されたスコープ(アクティビティまたはフラグメント)に関連付けて ViewModel インスタンスを作成します。
  • 作成された ViewModel は、スコープが存続している限り保持されます。たとえば、スコープがフラグメントの場合、ViewModel はフラグメントがデタッチされるまで保持されます。

ViewModelProviders.of() メソッドを使用して ViewModelProvider を作成し、ViewModel を初期化します。

  1. GameFragment クラスで、viewModel 変数を初期化します。このコードを onCreateView() のバインディング変数の定義の後に追加します。ViewModelProviders.of() メソッドを使用し、関連する GameFragment コンテキストと GameViewModel クラスを渡します。
  2. ViewModel オブジェクトの初期化の上に、ViewModelProviders.of() メソッド呼び出しをログに記録するログ ステートメントを追加します。
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. アプリを実行します。Android Studio で [Logcat] ペインを開き、Game でフィルタします。デバイスまたはエミュレータの [再生] ボタンをタップします。ゲーム画面が開きます。

    Logcat に示されているように、GameFragmentonCreateView() メソッドが ViewModelProviders.of() メソッドを呼び出して GameViewModel を作成します。GameFragmentGameViewModel に追加したロギング ステートメントが Logcat に表示されます。

  1. デバイスやエミュレータで自動回転設定を有効にして、画面の向きを数回変更します。GameFragment は毎回破棄されて再作成されるため、ViewModelProviders.of() は毎回呼び出されます。ただし、GameViewModel は一度だけ作成され、呼び出しごとに再作成または破棄されることはありません。
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. ゲームを終了するか、ゲーム フラグメントから移動します。GameFragment が破棄されます。関連付けられている GameViewModel も破棄され、コールバック onCleared() が呼び出されます。
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

ViewModel は構成の変更後も保持されるため、構成の変更後も保持する必要があるデータを保存するのに適しています。

  • 画面に表示するデータと、そのデータを処理するコードを ViewModel に配置します。
  • アクティビティ、フラグメント、ビューは構成の変更後も存続しないため、ViewModel にフラグメント、アクティビティ、ビューへの参照を含めることはできません。

比較のため、ViewModel を追加する前と追加した後のスターター アプリでの GameFragment UI データの処理方法を以下に示します。ViewModel

  • ViewModel を追加する前:
    画面の回転などの構成変更が発生すると、ゲーム フラグメントが破棄され、再作成されます。データが失われます。
  • ViewModel を追加してゲーム フラグメントの UI データを ViewModel に移動すると、フラグメントで表示する必要があるすべてのデータが ViewModel になります。
    アプリで構成変更が発生しても、ViewModel は存続し、データは保持されます。

このタスクでは、アプリの UI データと、データを処理するメソッドを GameViewModel クラスに移動します。これは、構成変更時にデータが保持されるようにするためです。

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

次のデータ フィールドとメソッドを GameFragment から GameViewModel に移動します。

  1. wordscorewordList のデータ フィールドを移動します。wordscoreprivate でないことを確認します。

    バインディング変数 GameFragmentBinding にはビューへの参照が含まれているため、移動しないでください。この変数は、レイアウトの拡張、クリック リスナーの設定、画面へのデータの表示(フラグメントの役割)に使用されます。
  2. resetList() メソッドと nextWord() メソッドを移動します。これらのメソッドは、画面に表示する単語を決定します。
  3. onCreateView() メソッド内で、resetList()nextWord() へのメソッド呼び出しを GameViewModelinit ブロックに移動します。

    これらのメソッドは init ブロックに含める必要があります。これは、フラグメントが作成されるたびにではなく、ViewModel が作成されるときに単語リストをリセットする必要があるためです。GameFragmentinit ブロックでログ ステートメントを削除できます。

GameFragmentonSkip()onCorrect() のクリック ハンドラには、データを処理して UI を更新するコードが含まれています。UI を更新するコードはフラグメントに残す必要がありますが、データを処理するコードは ViewModel に移動する必要があります。

今のところ、両方に同じメソッドを配置します。

  1. onSkip() メソッドと onCorrect() メソッドを GameFragment から 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() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           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 ViewModelProviders.of")
       viewModel = ViewModelProviders.of(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() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           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 に移動したため、ゲーム フラグメントからアクセスできなくなりました。

    GameFragmentonSkip() メソッドと onCorrect() メソッドで、nextWord() の呼び出しを updateScoreText()updateWordText() に置き換えます。これらのメソッドは、データを画面に表示します。
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()
}
  1. GameViewModelnextWord() メソッド内で、updateWordText() メソッドと updateScoreText() メソッドの呼び出しを削除します。これらのメソッドは GameFragment から呼び出されるようになりました。
  2. アプリをビルドして、エラーがないことを確認します。エラーが発生した場合は、プロジェクトをクリーンアップして再ビルドします。
  3. アプリを実行して、数語プレイします。ゲーム画面でデバイスを回転させます。向きを変更した後も現在のスコアと現在の単語が保持されています。

パフォーマンスは良好です。これで、アプリのデータはすべて ViewModel に保存されるため、構成変更後も保持されます。

このタスクでは、[End Game] ボタンのクリック リスナーを実装します。

  1. GameFragment で、onEndGame() というメソッドを追加します。ユーザーが [End Game] ボタンをタップすると、onEndGame() メソッドが呼び出されます。
private fun onEndGame() {
   }
  1. GameFragmentonCreateView() メソッド内で、[Got It] ボタンと [Skip] ボタンのクリック リスナーを設定するコードを探します。これらの 2 行のすぐ下に、[End Game] ボタンのクリック リスナーを設定します。バインディング変数 binding を使用します。クリック リスナー内で、onEndGame() メソッドを呼び出します。
binding.endGameButton.setOnClickListener { onEndGame() }
  1. GameFragment で、アプリをスコア画面に移動する gameFinished() というメソッドを追加します。Safe Args を使用して、スコアを引数として渡します。
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. onEndGame() メソッドで、gameFinished() メソッドを呼び出します。
private fun onEndGame() {
   gameFinished()
}
  1. アプリを実行し、ゲームをプレイして、いくつかの単語を切り替えます。[End Game ] ボタンをタップします。アプリはスコア画面に移動しますが、最終スコアは表示されません。これは次のタスクで修正します。

ユーザーがゲームを終了すると、ScoreFragment にスコアが表示されません。ScoreFragment で表示されるスコアを保持する ViewModel が必要です。ファクトリ メソッド パターンを使用して、ViewModel の初期化中にスコア値を渡します。

ファクトリ メソッド パターンは、ファクトリ メソッドを使用してオブジェクトを作成する作成デザイン パターンです。ファクトリ メソッドは、同じクラスのインスタンスを返すメソッドです。

このタスクでは、スコア フラグメントのパラメータ化されたコンストラクタと ViewModel をインスタンス化するファクトリ メソッドを使用して ViewModel を作成します。

  1. score パッケージの下に、ScoreViewModel という名前の新しい Kotlin クラスを作成します。このクラスは、スコア フラグメントの ViewModel になります。
  2. ViewModel. から ScoreViewModel クラスを拡張します。最終スコアのコンストラクタ パラメータを追加します。ログ ステートメントを含む init ブロックを追加します。
  3. ScoreViewModel クラスに、最終スコアを保存する score という変数を追加します。
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. score パッケージの下に、ScoreViewModelFactory という別の Kotlin クラスを作成します。このクラスは、ScoreViewModel オブジェクトのインスタンス化を担当します。
  2. ViewModelProvider.Factory から ScoreViewModelFactory クラスを拡張します。最終スコアのコンストラクタ パラメータを追加します。
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. ScoreViewModelFactory では、Android Studio に未実装の抽象メンバーに関するエラーが表示されます。エラーを解決するには、create() メソッドをオーバーライドします。create() メソッドで、新しく構築された ScoreViewModel オブジェクトを返します。
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. ScoreFragment で、ScoreViewModelScoreViewModelFactory のクラス変数を作成します。
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. ScoreFragmentonCreateView() 内で、binding 変数を初期化した後、viewModelFactory を初期化します。ScoreViewModelFactory を使用します。引数バンドルから最終スコアを ScoreViewModelFactory() のコンストラクタ パラメータとして渡します。
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. onCreateView( で、viewModelFactory を初期化した後、viewModel オブジェクトを初期化します。ViewModelProviders.of() メソッドを呼び出し、関連付けられたスコア フラグメント コンテキストと viewModelFactory を渡します。これにより、viewModelFactory クラス.で定義されたファクトリ メソッドを使用して ScoreViewModel オブジェクトが作成されます。
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. onCreateView() メソッドで、viewModel を初期化した後、scoreText ビューのテキストを ScoreViewModel で定義された最終スコアに設定します。
binding.scoreText.text = viewModel.score.toString()
  1. アプリを実行して、ゲームをプレイします。単語をいくつかまたはすべて確認し、[End Game] をタップします。スコア フラグメントに最終スコアが表示されるようになりました。

  1. 省略可: ScoreViewModel でフィルタリングして、Logcat の ScoreViewModel ログを確認します。スコア値が表示されます。
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

このタスクでは、ViewModel を使用するために ScoreFragment を実装しました。また、ViewModelFactory インターフェースを使用して ViewModel のパラメータ化されたコンストラクタを作成する方法も学びました。

これで、アプリのアーキテクチャを変更して、Android アーキテクチャ コンポーネントの 1 つである ViewModel を使用するようにした。アプリのライフサイクルに関する問題が解決され、ゲームのデータが構成変更後も引き継がれるようになりました。また、ViewModelFactory インターフェースを使用して ViewModel を作成するためのパラメータ化されたコンストラクタを作成する方法も学びました。

Android Studio プロジェクト: GuessTheWord

  • Android のアプリ アーキテクチャ ガイドラインでは、異なる役割を持つクラスに分割することを推奨しています。
  • UI コントローラは、ActivityFragment などの UI ベースのクラスです。UI コントローラには、UI やオペレーティング システムとのやり取りを処理するロジックのみを含めるべきです。UI に表示するデータを含めるべきではありません。そのデータを ViewModel に入れます。
  • ViewModel クラスは、UI 関連のデータを保存し管理します。ViewModel クラスを使用すると、画面の回転などの構成変更後もデータを維持することができます。
  • ViewModel は、推奨される Android アーキテクチャ コンポーネントの 1 つです。
  • ViewModelProvider.Factory は、ViewModel オブジェクトの作成に使用できるインターフェースです。

次の表は、UI コントローラと、それらのデータを保持する ViewModel インスタンスを比較したものです。

UI コントローラ

ViewModel

UI コントローラの例としては、この Codelab で作成した ScoreFragment があります。

ViewModel の例としては、この Codelab で作成した ScoreViewModel があります。

UI に表示するデータは含まれていません。

UI コントローラが UI に表示するデータが含まれます。

データを表示するコードと、クリック リスナーなどのユーザー イベント コードが含まれています。

データ処理のコードが含まれています。

構成が変更されるたびに破棄され、再作成されます。

関連付けられた UI コントローラが完全に消滅したとき(アクティビティの場合はアクティビティが終了したとき、フラグメントの場合はフラグメントがデタッチされたとき)にのみ破棄されます。

ビューが含まれています。

アクティビティ、フラグメント、ビューへの参照を含めることはできない。これらは構成の変更後も存続しないが、ViewModel は存続するため。

関連する ViewModel への参照が含まれます。

関連する UI コントローラへの参照が含まれていません。

Udacity コース:

Android デベロッパー ドキュメント:

その他:

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

以下の質問に回答してください

問題 1

デバイス設定の変更時にデータが失われないようにするには、どのクラスにアプリデータを保存する必要がありますか。

  • ViewModel
  • LiveData
  • Fragment
  • Activity

問題 2

ViewModel に、フラグメント、アクティビティ、ビューへの参照を含めることはできない。正誤問題

  • 正しい
  • 誤り

問題 3

ViewModel はどのようなときに破棄されますか。

  • デバイスの画面の向きの変更中に、関連する UI コントローラが破棄され、再作成されたとき。
  • 画面の向きを変更しているとき。
  • 関連する UI コントローラが終了したとき(アクティビティの場合)、またはデタッチされたとき(フラグメントの場合)。
  • ユーザーが [戻る] ボタンを押したとき。

問題 4

ViewModelFactory インターフェースの用途は何ですか。

  • ViewModel オブジェクトをインスタンス化する。
  • 画面の向きの変更中にデータを保持する。
  • 画面に表示されているデータを更新する。
  • アプリデータが変更されたときに通知を受け取る。

次のレッスンに進む: 5.2: LiveData と LiveData のオブザーバー

このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。