この Codelab は、Android Kotlin の基礎コースの一部です。Codelab を順番に進めていくと、このコースを最大限に活用できます。すべてのコース Codelab は Android Kotlin の基礎 Codelab ランディング ページに掲載されています。
タイトル画面 | ゲーム画面 | スコア画面 |
はじめに
この Codelab では、Android アーキテクチャ コンポーネントの一つである ViewModel
について学習します。
ViewModel
クラスを使用して、ライフサイクルを意識した方法で UI 関連のデータを保存し管理します。ViewModel
クラスを使用すると、画面の回転やキーボードの可用性の変更など、デバイス構成の変更後もデータを維持できます。ViewModelFactory
クラスを使用して、構成変更後も保持されるViewModel
オブジェクトをインスタンス化して返します。
前提となる知識
- Kotlin で基本的な Android アプリを作成する方法。
- ナビゲーション グラフを使用してアプリにナビゲーションを実装する方法。
- アプリのデスティネーション間を移動してナビゲーション デスティネーション間でデータを渡すコードを追加する方法。
- アクティビティとフラグメントのライフサイクルの仕組み
- Android Studio の Logcat を使用して、アプリにロギング情報を追加し、ログを読み取る方法。
学習内容
- 推奨される Android アプリ アーキテクチャの使用方法。
- アプリで
Lifecycle
、ViewModel
、ViewModelFactory
クラスを使用する方法 - デバイス設定の変更によって UI データを維持する方法
- ファクトリー メソッドの設計パターンとその使用方法。
- インターフェース
ViewModelProvider.Factory
を使用してViewModel
オブジェクトを作成する方法。
演習内容
- アプリに
ViewModel
を追加して、設定の変更後にもデータが保存されるようにアプリデータを保存します。 ViewModelFactory
と、ファクトリ メソッドの設計パターンを使用して、コンストラクタ パラメータを含むViewModel
オブジェクトをインスタンス化します。
レッスン 5 の Codelab では、スターター コードから GuessTheWord アプリを開発します。GuessTheWord は 2 人構成のキャラクター スタイル ゲームで、プレーヤーが協力して最高スコアを獲得します。
1 人目のプレーヤーはアプリで単語を確認し、2 人目のプレーヤーに単語を表示しないように順番に実行します。2 人目のプレーヤーがその単語を推測します。
ゲームをプレイするには、最初のプレーヤーがデバイスでアプリを開き、以下のスクリーンショットに示すように「ギター」などの単語が表示されます。
1 人目のプレーヤーが単語を実際に操作し、単語そのものを言わないように注意します。
- 2 人目のプレーヤーが単語を正しく推測したら、[OK] ボタンを押すと、カウントが 1 つ増えて次の単語が表示されます。
- 2 人目のプレーヤーが単語を推測できない場合は、[スキップ] ボタンを押すと、カウントが 1 つ減って次の単語に進みます。
- ゲームを終了するには、[ゲームを終了] ボタンを押します。(この機能は、シリーズの最初の Codelab のスターター コードにはありません)。
このタスクでは、スターター アプリをダウンロードして実行し、コードを調べます。
ステップ 1: スタートガイド
- GuesTheWord スターター コードをダウンロードし、Android Studio でプロジェクトを開きます。
- Android 搭載デバイスまたはエミュレータでアプリを実行します。
- ボタンをタップします。[Skip] ボタンでは次の単語を表示し、スコアを 1 つ減らします。[Got It] ボタンでは次の単語を表示してスコアを 1 つ増やします。[ゲームを終了] ボタンは実装されていないため、タップしても何も起こりません。
ステップ 2: コードのチュートリアルを実施する
- Android Studio でコードを確認し、アプリの動作を実感してください。
- 以下の重要なファイルについては、必ずご確認ください。
MainActivity.kt
このファイルには、テンプレートで生成されたデフォルトのコードのみが含まれています。
res/layout/main_activity.xml
このファイルには、アプリのメイン レイアウトが含まれています。NavHostFragment
は、ユーザーがアプリ内を移動したときに他のフラグメントをホストします。
UI フラグメント
スターター コードには、com.example.android.guesstheword.screens
パッケージ内の 3 つのパッケージに 3 つのフラグメントがあります。
title/TitleFragment
: タイトル画面game/GameFragment
: ゲーム画面score/ScoreFragment
(スコア画面用)
screen/title/TitleFragment.kt
タイトル フラグメントは、アプリの起動時に最初に表示される画面です。クリック ハンドラが [Play] ボタンに設定されており、ゲーム画面に移動しています。
screen/game/GameFragment.kt
これは、ゲームのアクションの大部分が発生するメイン フラグメントです。
- 変数は、現在の単語と現在のスコアに対して定義されます。
resetList()
メソッド内で定義されたwordList
は、ゲームで使用される単語のサンプルリストです。onSkip()
メソッドは、[Skip] ボタンのクリック ハンドラです。スコアを 1 減らし、nextWord()
メソッドを使用して次の単語を表示します。onCorrect()
メソッドは、[OK] ボタンのクリック ハンドラです。このメソッドは、onSkip()
メソッドと同様に実装されます。唯一の違いは、この方法では減算ではなく、スコアに 1 を加算することです。
screen/score/ScoreFragment.kt
ScoreFragment
はゲームの最終画面であり、プレーヤーの最終スコアが表示されます。この Codelab では、この画面と最終スコアを表示する実装を追加します。
res/navigation/main_navigation.xml
ナビゲーション グラフは、ナビゲーションによってフラグメントがどのように接続されているかを示しています。
- title フラグメントから、ユーザーはゲーム フラグメントに移動できます。
- ゲーム フラグメントから、スコア フラグメントに移動できます。
- スコア フラグメントから、ゲーム フラグメントに戻ることができます。
このタスクでは、GuesTheWord スターター アプリに関する問題を見つけます。
- スターター コードを実行し、単語ごとにゲームを実行してから [スキップ] または [OK] をタップします。
- ゲーム画面に単語と現在のスコアが表示されます。デバイスまたはエミュレータを回転して、画面の向きを変更すると、現在のスコアが失われます。
- ゲームをさらに数語実行します。ゲーム画面になんらかのスコアが表示されたら、アプリを閉じて、もう一度開きます。ゲームの状態は保存されていないため、ゲームは最初からやり直しになります。
- 数単語プレイしてから [ゲームを終了] ボタンをタップします。何も起こりません。
アプリに関する問題:
- スターター アプリは、デバイスの画面の向きが変化したときや、アプリがシャットダウンして再起動したときなど、設定の変更時にアプリの状態を保存および復元しません。
この問題を解決するには、onSaveInstanceState()
コールバックを使用します。ただし、onSaveInstanceState()
メソッドを使用するには、状態をバンドルに保存するための追加のコードを記述し、その状態を取得するためのロジックを実装する必要があります。また、保存できるデータの量は最小限です。 - ユーザーが [ゲームを終了] ボタンをタップしても、ゲーム画面がスコア画面に移動しない。
この問題を解決するには、この Codelab で学習するアプリ アーキテクチャ コンポーネントを使用します。
アプリ アーキテクチャ
アプリ アーキテクチャは、アプリを設計し、クラス間、およびそれらの関係性を設計する方法です。コードは体系的に整理され、特定のシナリオで適切に実行され、操作も簡単です。この 4 つの Codelab のシリーズでは、GuesTheWord アプリに加えられた改善が Android アプリ アーキテクチャのガイドラインを遵守し、Android アーキテクチャ コンポーネントを使用します。Android アプリのアーキテクチャは、MVVM(model-view-viewmodel)のアーキテクチャ パターンに似ています。
GuessTheWord アプリは関心の分離の設計原則に従い、クラスに分かれており、クラスごとに別々の問題に対処しています。このレッスンの最初の Codelab では、UI コントローラ、ViewModel
、ViewModelFactory
を使用します。
UI コントローラ
UI コントローラは、Activity
や Fragment
などの UI ベースのクラスです。UI コントローラには、ビューの表示やユーザー入力のキャプチャなど、UI やオペレーティング システムのインタラクションを処理するロジックのみを含めるべきです。表示するテキストを決定するロジックなどの意思決定ロジックを UI コントローラに配置しないでください。
GuesTheWord スターター コードでは、UI コントローラは GameFragment
、ScoreFragment,
、TitleFragment
の 3 つのフラグメントになります。「関心の分離」の設計原則に従う場合、GameFragment
はゲーム要素を画面に描画し、ユーザーがボタンをタップするタイミングだけを認識するだけです。ユーザーがボタンをタップすると、その情報が GameViewModel
に渡されます。
ViewModel
ViewModel
は、ViewModel
に関連付けられたフラグメントまたはアクティビティに表示されるデータを保持します。ViewModel
は、データを簡単に計算して変換し、UI コントローラが表示するデータを準備できるようにします。このアーキテクチャでは、ViewModel
が意思決定を実行します。GameViewModel
は、スコア値、単語のリスト、現在の単語などのデータを保持します。これが画面に表示されるデータであるためです。GameViewModel
には、簡単な計算を実行してデータの現在の状態を判断するビジネス ロジックも含まれています。
ViewModelFactory
ViewModelFactory
は、コンストラクタ パラメータの有無にかかわらず、ViewModel
オブジェクトをインスタンス化します。
後の Codelab では、UI コントローラと ViewModel
に関連するその他の Android アーキテクチャ コンポーネントについて学習します。
ViewModel
クラスは、UI 関連のデータを保存し管理するためのように設計されています。このアプリでは、各 ViewModel
は 1 つのフラグメントに関連付けられています。
このタスクでは、最初の ViewModel
をアプリ(GameFragment
の GameViewModel
)に追加します。また、ViewModel
がライフサイクル対応であることの意味も学習します。
ステップ 1: GameViewModel クラスを追加する
build.gradle(module:app)
ファイルを開きます。dependencies
ブロック内で、ViewModel
の Gradle 依存関係を追加します。
最新バージョンのライブラリを使用すると、ソリューション アプリは期待どおりにコンパイルされます。解決しない場合は、問題を解決するか、下記のバージョンに戻してください。
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
screens/game/
パッケージで、GameViewModel
という新しい Kotlin クラスを作成します。GameViewModel
クラスは抽象クラスViewModel
を拡張します。ViewModel
がライフサイクル対応である点について理解を深めるには、log
ステートメントでinit
ブロックを追加します。
class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}
ステップ 2: onCleared() をオーバーライドしてロギングを追加する
ViewModel
は、関連付けられたフラグメントの接続が解除されたとき、またはアクティビティが終了したときに破棄されます。ViewModel
が破棄される直前には、onCleared()
コールバックが呼び出され、リソースがクリーンアップされます。
GameViewModel
クラスのonCleared()
メソッドをオーバーライドします。onCleared()
内にログ ステートメントを追加して、GameViewModel
のライフサイクルを追跡します。
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
ステップ 3: GameViewModel をゲーム フラグメントに関連付ける
ViewModel
を UI コントローラに関連付ける必要があります。この 2 つを関連付けるために、UI コントローラ内に ViewModel
への参照を作成します。
このステップでは、対応する UI コントローラ(GameFragment
)内に GameViewModel
の参照を作成します。
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()
メソッドを使用して、ViewModel
を初期化して ViewModelProvider
を作成します。
GameFragment
クラスで、viewModel
変数を初期化します。このコードを、バインディング変数の定義の後にonCreateView()
内に配置します。ViewModelProviders.of()
メソッドを使用して、関連するGameFragment
コンテキストとGameViewModel
クラスを渡します。ViewModel
オブジェクトの初期化の上に、ViewModelProviders.of()
メソッド呼び出しをログに記録するログ ステートメントを追加します。
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
- アプリを実行します。Android Studio で [Logcat] ペインを開き、
Game
でフィルタします。デバイスまたはエミュレータの [Play] ボタンをタップします。ゲーム画面が開きます。
Logcat に示されているように、GameFragment
のonCreateView()
メソッドはViewModelProviders.of()
メソッドを呼び出してGameViewModel
を作成します。GameFragment
とGameViewModel
に追加したロギング ステートメントが Logcat に表示されます。
- デバイスやエミュレータで自動回転設定を有効にして、画面の向きを数回変更します。
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
- ゲームを終了するか、ゲーム フラグメントから移動します。
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
を追加する前と ViewModel
を追加した後のスターター アプリでの GameFragment
UI データの処理を次に示します。
ViewModel
を追加する前:
画面の回転などの構成変更が行われると、ゲーム フラグメントが破棄されて再作成されます。データが失われます。ViewModel
を追加して、ゲーム フラグメントの UI データをViewModel
:
に移動させると、フラグメントが表示する必要のあるデータはすべてViewModel
になります。アプリで構成の変更が行われても、ViewModel
は保持され、データは保持されます。
このタスクでは、アプリの UI データを、データを処理するメソッドとともに GameViewModel
クラスに移動します。これにより、構成変更時にもデータが保持されます。
ステップ 1: データ フィールドとデータ処理を ViewModel に移動する
次のデータ フィールドとメソッドを GameFragment
から GameViewModel
に移動します。
word
、score
、wordList
のデータ フィールドを移動します。word
とscore
がprivate
になっていないことを確認します。
バインディング変数GameFragmentBinding
にはビューへの参照が含まれているため、移動しないでください。この変数は、レイアウトをインフレートし、クリック リスナーを設定して、画面にデータ(フラグメントの役割)を表示するために使用します。resetList()
メソッドとnextWord()
メソッドを移動します。これらのメソッドは、画面に表示する単語を決定します。onCreateView()
メソッド内から、メソッド呼び出しresetList()
とnextWord()
をGameViewModel
のinit
ブロックに移動します。
これらのメソッドはinit
ブロックに配置する必要があります。これは、フラグメントが作成されるたびにではなく、ViewModel
が作成された際に単語リストをリセットする必要があるためです。ログ ステートメントはGameFragment
のinit
ブロックで削除できます。
GameFragment
の onSkip()
および onCorrect()
クリック ハンドラには、データを処理して UI を更新するためのコードが含まれています。UI を更新するコードはフラグメントに残りますが、データを処理するコードは ViewModel
に移動する必要があります。
ここでは、両方のメソッドに同一のメソッドを含めます。
onSkip()
メソッドとonCorrect()
メソッドをGameFragment
から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() {
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 のクリック ハンドラとデータ フィールドへの参照を更新する
GameFragment
で、onSkip()
メソッドとonCorrect()
メソッドを更新します。コードを削除してスコアを更新し、代わりにviewModel
で対応するonSkip()
とonCorrect()
メソッドを呼び出します。nextWord()
メソッドをViewModel
に移動したため、ゲーム フラグメントはアクセスできなくなります。GameFragment
のonSkip()
メソッドとonCorrect()
メソッドで、nextWord()
の呼び出しをupdateScoreText()
とupdateWordText()
に置き換えます。以下のメソッドはデータを画面に表示します。
private fun onSkip() {
viewModel.onSkip()
updateWordText()
updateScoreText()
}
private fun onCorrect() {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}
GameFragment
で、GameViewModel
変数を使用するようにscore
変数とword
変数を更新します。これらの変数はGameViewModel
内にあるからです。
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
GameViewModel
のnextWord()
メソッド内で、updateWordText()
メソッドとupdateScoreText()
メソッドの呼び出しを削除します。これらのメソッドはGameFragment
から呼び出されるようになりました。- アプリをビルドして、エラーがないことを確認します。エラーが発生した場合は、プロジェクトをクリーンアップして再ビルドします。
- アプリを実行して、いくつかの単語でゲームをプレイします。ゲーム画面を開いた状態でデバイスを回転させます。向きの変更後も現在のスコアと現在の単語は保持されます。
これでこれで、すべてのアプリデータが ViewModel
に保存され、設定変更時にも保持されます。
このタスクでは、[ゲーム終了] ボタンのクリック リスナーを実装します。
GameFragment
に、onEndGame()
というメソッドを追加します。ユーザーが [ゲームを終了] ボタンをタップすると、onEndGame()
メソッドが呼び出されます。
private fun onEndGame() {
}
GameFragment
のonCreateView()
メソッド内で、[Got It] ボタンと [Skip] ボタンのクリック リスナーを設定するコードを見つけます。この 2 行のすぐ下に、[ゲーム終了] ボタンのクリック リスナーを設定します。バインディング変数binding
を使用します。クリック リスナー内で、onEndGame()
メソッドを呼び出します。
binding.endGameButton.setOnClickListener { onEndGame() }
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)
}
onEndGame()
メソッドで、gameFinished()
メソッドを呼び出します。
private fun onEndGame() {
gameFinished()
}
- アプリを実行して、ゲームをプレイし、いくつかの単語を順番に繰り返します。[ゲームを終了] ボタンをタップします。アプリがスコア画面に移動したが、最終スコアが表示されないことに注意してください。これは次のタスクで修正します。
ユーザーがゲームを終了したとき、ScoreFragment
にスコアが表示されない。ScoreFragment
でスコアを表示するには、ViewModel
を設定する必要があります。ファクトリ メソッド パターンを使用して、ViewModel
の初期化中にスコア値を渡します。
ファクトリ メソッド パターンは、ファクトリ メソッドを使用してオブジェクトを作成する創造設計パターンです。ファクトリ メソッドは、同じクラスのインスタンスを返すメソッドです。
このタスクでは、スコア フラグメントのパラメータ化されたコンストラクタと、ViewModel
をインスタンス化するためのファクトリ メソッドを使用して、ViewModel
を作成します。
score
パッケージの下に、ScoreViewModel
という新しい Kotlin クラスを作成します。このクラスは、スコア フラグメントのViewModel
になります。ViewModel.
からScoreViewModel
クラスを拡張し、最終スコアのコンストラクタ パラメータを追加します。ログ ステートメントを含むinit
ブロックを追加します。ScoreViewModel
クラスに、score
という変数を追加して最終スコアを保存します。
class ScoreViewModel(finalScore: Int) : ViewModel() {
// The final score
var score = finalScore
init {
Log.i("ScoreViewModel", "Final score is $finalScore")
}
}
score
パッケージの下に、ScoreViewModelFactory
という別の Kotlin クラスを作成します。このクラスは、ScoreViewModel
オブジェクトをインスタンス化します。ScoreViewModelFactory
クラスをViewModelProvider.Factory
から拡張します。最終スコアのコンストラクタ パラメータを追加します。
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
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")
}
ScoreFragment
で、ScoreViewModel
とScoreViewModelFactory
のクラス変数を作成します。
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
ScoreFragment
のonCreateView()
内で、binding
変数を初期化した後、viewModelFactory
を初期化します。ScoreViewModelFactory
を使用します。引数バンドルの最終スコアをコンストラクタ パラメータとしてScoreViewModelFactory()
に渡します。
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
onCreateView(
など)で、viewModelFactory
を初期化した後、viewModel
オブジェクトを初期化します。ViewModelProviders.of()
メソッドを呼び出して、関連するスコア フラグメント コンテキストとviewModelFactory
を渡します。これにより、viewModelFactory
クラス.
で定義されたファクトリ メソッドを使用して、ScoreViewModel
オブジェクトが作成されます。
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)
onCreateView()
メソッドで、viewModel
を初期化した後、scoreText
ビューのテキストをScoreViewModel
で定義された最終スコアに設定します。
binding.scoreText.text = viewModel.score.toString()
- アプリを実行してゲームをプレイします。単語の一部または全部を順番に確認して、[ゲームを終了] をタップします。スコア フラグメントに最終スコアが表示されます。
- 省略可:
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 コントローラは、
Activity
やFragment
などの UI ベースのクラスです。UI コントローラには、UI やオペレーティング システムとのやり取りを処理するロジックのみを含め、UI に表示するデータを含めないでください。そのデータをViewModel
に格納します。 ViewModel
クラスは、UI 関連のデータを保存し管理します。ViewModel
クラスを使用すると、画面の回転などの構成の変更後にデータを引き継ぐことができます。ViewModel
は、推奨される Android アーキテクチャ コンポーネントの 1 つです。ViewModelProvider.Factory
は、ViewModel
オブジェクトの作成に使用できるインターフェースです。
次の表は、UI コントローラとデータを保持する ViewModel
インスタンスを比較したものです。
UI コントローラ | ViewModel |
UI コントローラの例としては、この Codelab で作成した |
|
UI に表示されるデータは含まれません。 | UI コントローラが UI に表示するデータが含まれています。 |
データを表示するためのコードと、クリック リスナーなどのユーザー イベント コードが含まれます。 | データ処理用のコードが含まれています。 |
構成が変わるたびに破棄され、再作成されます。 | 関連する UI コントローラが完全に削除されたとき(アクティビティの場合、アクティビティが終了したとき、フラグメントがデタッチされたとき)にのみ破棄されます。 |
ビューを含みます。 | アクティビティ、フラグメント、ビューへの参照は含めないでください。これらは、設定変更後に保持されませんが、 |
関連する | 関連付けられた UI コントローラへの参照は含まれません。 |
Udacity コース:
Android デベロッパー ドキュメント:
- ViewModel の概要
- ライフサイクル対応コンポーネントによるライフサイクルへの対応
- アプリ アーキテクチャ ガイド
ViewModelProvider
ViewModelProvider.Factory
その他:
- MVVM(model-view-viewmodel)アーキテクチャ パターン。
- 関心の分離(SoC)設計の原則
- ファクトリ メソッド パターン
このセクションでは、インストラクターが主導するコースの一環として、この Codelab に取り組む生徒の課題について説明します。教師は以下のことを行えます。
- 必要に応じて課題を割り当てます。
- 宿題の提出方法を生徒に伝える。
- 宿題を採点します。
教師はこれらの提案を少しだけ使うことができます。また、他の課題は自由に割り当ててください。
この Codelab にご自分で取り組む場合は、これらの課題を使用して知識をテストしてください。
次の質問に答えてください。
問題 1
デバイス設定の変更中にデータが失われないようにするには、アプリデータをどのクラスに保存する必要がありますか。
ViewModel
LiveData
Fragment
Activity
質問 2
ViewModel
には、フラグメント、アクティビティ、ビューへの参照を含めることはできません。正誤問題
- True
- False
問題 3
ViewModel
が破棄されるのはどのようなときですか。
- デバイスの画面の向きの変更中に、関連する UI コントローラが破棄され、再作成されたとき。
- 向きの変更
- 関連する UI コントローラが終了したとき(アクティビティの場合)、またはデタッチされたとき(フラグメントの場合)。
- ユーザーが戻るボタンを押すと、
問題 4
ViewModelFactory
インターフェースの目的は何ですか。
ViewModel
オブジェクトをインスタンス化する- 画面の向きが変化してもデータを保持する。
- 画面に表示されているデータを更新します。
- アプリデータが変更されたときに通知を受け取る。
次のレッスンを開始する:
このコースの他の Codelab へのリンクについては、Android Kotlin の基礎 Codelab ランディング ページをご覧ください。