この Codelab は、Android Kotlin の基礎コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。コースのすべての Codelab は、Android Kotlin の基礎の Codelab のランディング ページに一覧表示されています。
タイトル画面 | ゲーム画面 | スコア画面 |
はじめに
この Codelab では、Android アーキテクチャ コンポーネントの 1 つである 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 人のプレーヤーが最高スコアを目指して協力する、2 プレーヤー ジェスチャー ゲームです。
最初のプレーヤーはアプリの単語を見て、2 番目のプレーヤーに単語を見せないようにしながら、各単語を順番に演じます。2 人目のプレーヤーが単語を当てようとします。
ゲームをプレイするには、最初のプレーヤーがデバイスでアプリを開き、下のスクリーンショットに示すように、「ギター」などの単語を表示します。
最初のプレーヤーは、単語自体を言わないように注意しながら、単語を演じます。
- 2 人目のプレーヤーが単語を正しく当てると、1 人目のプレーヤーが [Got It] ボタンを押します。これにより、カウントが 1 つ増え、次の単語が表示されます。
- 2 人目のプレーヤーが単語を当てられない場合、1 人目のプレーヤーが [スキップ] ボタンを押します。これにより、カウントが 1 減り、次の単語にスキップします。
- ゲームを終了するには、[End Game] ボタンを押します。(この機能は、シリーズの最初の Codelab のスターター コードには含まれていません)。
このタスクでは、スターター アプリをダウンロードして実行し、コードを確認します。
ステップ 1: 開始する
- GuessTheWord スターター コードをダウンロードして、Android Studio でプロジェクトを開きます。
- Android 搭載デバイスまたはエミュレータでアプリを実行します。
- ボタンをタップします。[Skip] ボタンをタップすると、次の単語が表示され、スコアが 1 減ります。[Got It] ボタンをタップすると、次の単語が表示され、スコアが 1 増えます。[End Game] ボタンは実装されていないため、タップしても何も起こりません。
ステップ 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
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 スターター アプリの問題を見つけます。
- スターター コードを実行し、数個の単語をプレイします。各単語の後に [Skip] または [Got It] をタップします。
- ゲーム画面に単語と現在のスコアが表示されるようになりました。デバイスまたはエミュレータを回転して、画面の向きを変更すると、現在のスコアが失われます。
- ゲームを数語プレイします。ゲーム画面にスコアが表示されたら、アプリを閉じて再び開きます。アプリの状態が保存されていないため、ゲームが最初から再開されることに注目してください。
- 数個の単語をプレイしてから、[End Game] ボタンをタップします。何も起こらないことを確認します。
アプリに関する問題:
- スターター アプリでは、デバイスの画面の向きが変化したときや、アプリがシャットダウンして再起動したときなどの構成変更時に、アプリの状態が保存されません。
この問題は、onSaveInstanceState()
コールバックを使用して解決できます。ただし、onSaveInstanceState()
メソッドを使用する場合、バンドルに状態を保存するためのコードや、その状態を取得するロジックを実装する必要があります。また、保存できるデータの量は最小限です。 - ユーザーが [End Game] ボタンをタップしても、ゲーム画面がスコア画面に移動しません。
この問題を解決するには、この Codelab で学習するアプリ アーキテクチャ コンポーネントを使用します。
アプリ アーキテクチャ
アプリ アーキテクチャは、コードが整理され、特定のシナリオでパフォーマンスが向上し、操作が容易になるように、アプリのクラスとその関係を設計する方法です。この 4 つの Codelab では、GuessTheWord アプリに加える改善は Android アプリ アーキテクチャのガイドラインに沿っており、Android アーキテクチャ コンポーネントを使用します。Android アプリのアーキテクチャは、MVVM(モデル - ビュー - ビューモデル)アーキテクチャ パターンに似ています。
GuessTheWord アプリは、関心の分離という設計原則に従い、クラスに分割されています。各クラスは別々の関心事に対応しています。このレッスン最初の Codelab では、UI コントローラ、ViewModel
、ViewModelFactory
を使用します。
UI コントローラ
UI コントローラは、Activity
や Fragment
などの UI ベースのクラスです。UI コントローラには、ビューの表示やユーザー入力の取得など、UI やオペレーティング システムとのやり取りを処理するロジックのみを含めるべきです。表示するテキストを決定するロジックなどの意思決定ロジックは、UI コントローラに入れないでください。
GuessTheWord スターター コードでは、UI コントローラは GameFragment
、ScoreFragment,
、TitleFragment
の 3 つのフラグメントです。「関心の分離」という設計原則に従い、GameFragment
はゲーム要素を画面に描画し、ユーザーがボタンをタップしたタイミングを把握するだけで、それ以上のことは行いません。ユーザーがボタンをタップすると、この情報が GameViewModel
に渡されます。
ViewModel
ViewModel
は、関連付けられたフラグメントまたはアクティビティに表示されるデータを保持します。ViewModel
また、UI コントローラがデータを表示するための前処理として、簡単な計算と変換を行えます。ViewModel
このアーキテクチャでは、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()
メソッドを使用して ViewModelProvider
を作成し、ViewModel
を初期化します。
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
でフィルタします。デバイスまたはエミュレータの [再生] ボタンをタップします。ゲーム画面が開きます。
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
を追加する前と追加した後のスターター アプリでの GameFragment
UI データの処理方法を以下に示します。ViewModel
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
で、score
変数とword
変数を更新してGameViewModel
変数を使用します。これらの変数はGameViewModel
に含まれるようになったためです。
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
GameViewModel
のnextWord()
メソッド内で、updateWordText()
メソッドとupdateScoreText()
メソッドの呼び出しを削除します。これらのメソッドはGameFragment
から呼び出されるようになりました。- アプリをビルドして、エラーがないことを確認します。エラーが発生した場合は、プロジェクトをクリーンアップして再ビルドします。
- アプリを実行して、数語プレイします。ゲーム画面でデバイスを回転させます。向きを変更した後も現在のスコアと現在の単語が保持されています。
パフォーマンスは良好です。これで、アプリのデータはすべて ViewModel
に保存されるため、構成変更後も保持されます。
このタスクでは、[End Game] ボタンのクリック リスナーを実装します。
GameFragment
で、onEndGame()
というメソッドを追加します。ユーザーが [End Game] ボタンをタップすると、onEndGame()
メソッドが呼び出されます。
private fun onEndGame() {
}
GameFragment
のonCreateView()
メソッド内で、[Got It] ボタンと [Skip] ボタンのクリック リスナーを設定するコードを探します。これらの 2 行のすぐ下に、[End Game] ボタンのクリック リスナーを設定します。バインディング変数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()
}
- アプリを実行し、ゲームをプレイして、いくつかの単語を切り替えます。[End Game ] ボタンをタップします。アプリはスコア画面に移動しますが、最終スコアは表示されません。これは次のタスクで修正します。
ユーザーがゲームを終了すると、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
オブジェクトのインスタンス化を担当します。ViewModelProvider.Factory
からScoreViewModelFactory
クラスを拡張します。最終スコアのコンストラクタ パラメータを追加します。
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()
- アプリを実行して、ゲームをプレイします。単語をいくつかまたはすべて確認し、[End Game] をタップします。スコア フラグメントに最終スコアが表示されるようになりました。
- 省略可:
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(モデル-ビュー-ビューモデル)アーキテクチャ パターン。
- 関心の分離(SoC)の設計原則
- ファクトリー メソッド パターン
このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。
- 必要に応じて宿題を与える
- 宿題の提出方法を生徒に伝える
- 宿題を採点する
インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。
この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。
以下の質問に回答してください
問題 1
デバイス設定の変更時にデータが失われないようにするには、どのクラスにアプリデータを保存する必要がありますか。
ViewModel
LiveData
Fragment
Activity
問題 2
ViewModel
に、フラグメント、アクティビティ、ビューへの参照を含めることはできない。正誤問題
- 正しい
- 誤り
問題 3
ViewModel
はどのようなときに破棄されますか。
- デバイスの画面の向きの変更中に、関連する UI コントローラが破棄され、再作成されたとき。
- 画面の向きを変更しているとき。
- 関連する UI コントローラが終了したとき(アクティビティの場合)、またはデタッチされたとき(フラグメントの場合)。
- ユーザーが [戻る] ボタンを押したとき。
問題 4
ViewModelFactory
インターフェースの用途は何ですか。
ViewModel
オブジェクトをインスタンス化する。- 画面の向きの変更中にデータを保持する。
- 画面に表示されているデータを更新する。
- アプリデータが変更されたときに通知を受け取る。
次のレッスンに進む:
このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。