1. 始める前に
前の Codelab では、アクティビティとフラグメントのライフサイクル、および関連するライフサイクルの構成変更にともなう問題について学習しました。アプリデータを保存するには、インスタンスの状態を保存するのも 1 つの方法ですが、それには固有の制限があります。この Codelab では、Android Jetpack ライブラリを利用して、アプリを設計し、構成変更後もアプリデータを維持する堅牢な方法について学びます。
Android Jetpack ライブラリは、優れた Android アプリの開発を支援するライブラリ集です。このライブラリ集を使用すると、ベスト プラクティスに沿って開発を進めながら、ボイラープレート コードを作成する手間を省き、複雑なタスクを簡素化できるので、アプリのロジックなどのコードの重要な部分に集中できます。
Android アーキテクチャ コンポーネントは、Android Jetpack ライブラリに含まれ、優れたアーキテクチャを持つアプリの設計を支援します。アーキテクチャ コンポーネントは、アプリのアーキテクチャに関する指針を提供します。また、この指針は推奨されるベスト プラクティスに沿っています。
アプリ アーキテクチャは、デザインルールの集まりです。このアーキテクチャは、住宅の設計図と同じように、アプリに構造を与えます。優れたアプリ アーキテクチャは、コードの堅牢性、柔軟性、スケーラビリティ、メンテナンス性を長年にわたって高く維持します。
この Codelab では、アプリデータを保存するアーキテクチャ コンポーネントのひとつである ViewModel
の使用方法を学びます。フレームワークが構成変更やその他のイベント中にアクティビティとフラグメントを破棄して再作成しても、保存されているデータは失われません。
前提条件
- GitHub からソースコードをダウンロードして Android Studio で開けること
- Kotlin でアクティビティとフラグメントを使って基本的な Android アプリを作成し実行できること
- マテリアル テキスト フィールドと、一般的な UI ウィジェット(
TextView
やButton
など)に関する知識 - アプリでビュー バインディングを使用できること
- アクティビティとフラグメントのライフサイクルの基礎知識
- アプリにログ情報を追加し、Android Studio の Logcat を使用してログを読み取れること
学習内容
- Android アプリのアーキテクチャの基礎
- アプリで
ViewModel
クラスを使用する方法 ViewModel
を使用してデバイスの構成変更後も UI データを維持する方法- Kotlin のバッキング プロパティ
- マテリアル デザイン コンポーネント ライブラリの
MaterialAlertDialog
の使用方法
作成するアプリの概要
- スクランブルされた単語を推測する Unscramble ゲームアプリ
必要なもの
- Android Studio がインストールされているパソコン
- Unscramble アプリのスターター コード
2. スターター アプリの概要
ゲームの概要
Unscramble アプリは、1 人でプレイするスクランブラー ゲームです。アプリには、スクランブルされた単語が一度に 1 つ表示されます。プレーヤーは、スクランブルされた単語のすべての文字を使った単語を推測します。単語が正しい場合はスコアが加算されます。何回でも挑戦できます。現在の単語をスキップすることもできます。左上に単語カウントが表示されます。現在のゲームでプレイした単語の数です。1 ゲームにつき 10 単語です。
スターター コードをダウンロードする
この Codelab では、この Codelab で学んだ機能を使って拡張するためのスターター コードが提供されています。スターター コードには、以前の Codelab で学んだコードと学んでいないコードの両方が含まれている可能性があります。学んでいないコードの詳細については、今後いずれかの Codelab で学習できます。
GitHub のスターター コードを使用する場合、フォルダ名は android-basics-kotlin-unscramble-app-starter
になります。Android Studio でプロジェクトを開くときは、このフォルダを選択してください。
- プロジェクト用に提供されている GitHub リポジトリ ページに移動します。
- ブランチ名が Codelab で指定されたブランチ名と一致していることを確認します。たとえば、次のスクリーンショットでは、ブランチ名は main です。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ポップアップが表示されます。
- ポップアップで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ちます。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで、[Open] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [Open] を選択します。
- ファイル ブラウザで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開かれるまで待ちます。
- 実行ボタン をクリックして、アプリをビルドし、実行します。期待どおりにビルドされることを確認します。
スターター コードの概要
- Android Studio でスターター コードのプロジェクトを開きます。
- Android デバイスまたはエミュレータでアプリを実行します。
- [Submit] ボタンと [Skip] ボタンをタップして、数個の単語をプレイします。ボタンをタップすると、次の単語が表示され、単語カウントが増えます。
- [Submit] ボタンをタップした場合にのみスコアが増えることを確認します。
スターター コードの問題点
ゲームをプレイしていると、次のようなバグに気付くと思います。
- [Submit] ボタンをクリックしても、プレーヤーの単語がチェックされません。常に得点となります。
- ゲームを終了する方法がありません。10 単語を超えてもプレイできます。
- ゲーム画面にスクランブルされた単語、プレーヤーのスコア、単語が表示されますが、デバイスまたはエミュレータを回転して、画面の向きを変更すると、現在の単語、スコア、単語カウントが失われ、ゲームが最初からやり直しになります。
アプリの主な問題点
スターター アプリでは、デバイスの画面の向きが変化したときなどの構成変更時に、アプリの状態とデータが保存されません。
この問題は、onSaveInstanceState()
コールバックを使用することで解決できます。ただし、onSaveInstanceState()
メソッドを使用する場合、バンドルに状態を保存するためのコードや、その状態を取得するロジックを実装する必要があります。また、保存できるデータの量は最小限です。
この問題を解決するには、ここで学習する Android アーキテクチャ コンポーネントを使用します。
スターター コードのチュートリアル
ダウンロードしたスターター コードには、ゲーム画面のレイアウトがあらかじめ用意されています。ここでは、ゲームロジックの実装に焦点を当てます。アーキテクチャ コンポーネントを使用して、推奨されるアプリ アーキテクチャを実装し、上記の問題を解決します。作業の土台とするファイルの一部について簡単に説明します。
game_fragment.xml
- [Design] ビューで
res/layout/game_fragment.xml
を開きます。 - これには、ゲーム画面となるアプリの唯一の画面のレイアウトが含まれています。
- このレイアウトには、プレーヤーの単語のテキスト フィールドと、スコアと単語カウントを表示する
TextViews
が含まれています。ゲームをプレイするための手順とボタン([Submit] と [Skip])も表示されます。
main_activity.xml
1 つのゲーム フラグメントを持つメイン アクティビティのレイアウトを定義します。
res/values フォルダ
このフォルダにあるリソース ファイルについては学習済みです。
colors.xml
には、アプリで使用するテーマカラーが入っています。strings.xml
には、アプリに必要な文字列がすべて入っています。themes
フォルダとstyles
フォルダには、このアプリ用の UI のカスタマイズが入っています。
MainActivity.kt
アクティビティのコンテンツ ビューを main_activity.xml.
に設定する、デフォルト テンプレートで生成されたコードが入っています。
ListOfWords.kt
このファイルには、ゲームで使用されている単語のリストに加え、ゲームごとの最大単語数と正解するごとに加算される点数の定数が入っています。
GameFragment.kt
アプリ内の唯一のフラグメントであり、ゲームのアクションのほとんどはここで発生します。
- 現在のスクランブルされた単語(
currentScrambledWord
)、単語カウント(currentWordCount
)、スコア(score
)の変数が定義されています。 binding
という名前のgame_fragment
ビューにアクセスできるバインディング オブジェクトのインスタンスが定義されています。onCreateView()
関数は、バインディング オブジェクトを使用してgame_fragment
レイアウト XML をインフレートします。onViewCreated()
関数は、ボタンのクリック リスナーをセットアップし、UI を更新します。onSubmitWord()
は、[Submit] ボタンのクリック リスナーです。この関数は次のスクランブルされた単語を表示し、テキスト フィールドをクリアして、プレーヤーの単語を検証せずにスコアと単語カウントを増やします。onSkipWord()
は、[Skip] ボタンのクリック リスナーであり、この関数はスコアを除いてonSubmitWord()
と同じように UI を更新します。getNextScrambledWord()
は、単語リストからランダムに単語を選択し、その文字をシャッフルするヘルパー関数です。restartGame()
関数とexitGame()
関数は、それぞれゲームのやり直しと終了に使用します。これらの関数は後で使用します。setErrorTextField()
は、テキスト フィールドの内容を消去し、エラー ステータスをリセットします。updateNextWordOnScreen()
関数は、新しいスクランブルされた単語を表示します。
3.アプリ アーキテクチャの詳細
アーキテクチャは、各クラスにアプリ内での役割を割り当てるためのガイドラインを提供します。優れた設計のアプリ アーキテクチャでは、アプリの大規模化や機能追加による拡張が可能になります。また、チームの共同作業も容易になります。
最も一般的なアーキテクチャ原則は、関心の分離と、モデルで UI を操作することです。
関心の分離
関心の分離という設計原則では、アプリをクラスに分割する必要があり、それぞれに別々の役割を持たせる必要があるとされています。
UI をモデルで操作する
もう 1 つの重要な原則は、UI をモデルで操作することです。望ましいのは永続モデルです。モデルとは、アプリのデータ処理を担うコンポーネントです。モデルはアプリの Views
オブジェクトやアプリ コンポーネントから独立しているため、アプリのライフサイクルや関連する問題の影響を受けません。
Android アーキテクチャの主なクラスまたはコンポーネントは、UI コントローラ(アクティビティ / フラグメント)、ViewModel
、LiveData
、Room
です。これらのコンポーネントを使用することで、ライフサイクルの複雑さに対処し、ライフサイクルに関する問題を回避できます。LiveData
と Room
については、後の Codelab で学習します。
次の図は、アーキテクチャの基本部分を示しています。
UI コントローラ(アクティビティ / フラグメント)
アクティビティとフラグメントは UI コントローラです。UI コントローラは、画面へのビューの表示や、ユーザー イベントやユーザーが操作する UI に関連するその他の内容を捕捉することで、UI を制御します。アプリ内のデータやそのデータに関する意思決定ロジックは、UI コントローラ クラスに入れるべきではありません。
Android システムは、特定のユーザー操作に基づいて、またはシステム状態(メモリ不足など)を理由として、UI コントローラをいつでも破棄できます。デベロッパーはこうしたイベントを制御できないため、アプリのデータや状態を UI コントローラに保存してはなりません。代わりに、データに関する意思決定ロジックを ViewModel
に追加するべきです。
たとえば、Unscramble アプリでは、スクランブルされた単語、スコア、単語カウントがフラグメント(UI コントローラ)に表示されます。次のスクランブルされた単語の作成や、スコアと単語カウントの計算などの意思決定コードは、ViewModel
に入れるべきです。
ViewModel
ViewModel
は、ビューに表示されるアプリデータのモデルです。モデルは、アプリのデータ処理を担うコンポーネントです。これにより、アプリがアーキテクチャ原則に従い、モデルで UI を操作することが可能になります。
ViewModel
には、Android フレームワークによってアクティビティまたはフラグメントが破棄されて再作成されたときにも破棄されない、アプリ関連のデータが保存されます。ViewModel
オブジェクトは、構成変更時に自動的に保持されるため(アクティビティやフラグメントのインスタンスとは違って破棄されません)、このオブジェクトが保持しているデータは次のアクティビティやフラグメントのインスタンスですぐに利用できます。
アプリに ViewModel
を実装するには、アーキテクチャ コンポーネント ライブラリの ViewModel
クラスを拡張して、そのクラス内にアプリデータを保存します。
まとめ
フラグメント / アクティビティ(UI コントローラ)の役割 |
|
アクティビティとフラグメントは、画面へのビューやデータの描画や、ユーザー イベントへの応答を担います。 |
|
4. ViewModel を追加する
このタスクでは、アプリデータ(スクランブルされた単語、単語カウント、スコア)を保存する ViewModel
をアプリに追加します。
アプリのアーキテクチャは次のようになります。MainActivity
には GameFragment
が含まれ、GameFragment
は GameViewModel
のゲームに関する情報にアクセスします。
- Android Studio の [Android] ウィンドウで、[Gradle Scripts] フォルダにあるファイル
build.gradle(Module:Unscramble.app)
を開きます。 - アプリで
ViewModel
を使用するために、ViewModel ライブラリの依存関係がdependencies
ブロック内にあることを確認します。このステップはすでに完了しています。ライブラリの最新バージョンによっては、生成されるコードのライブラリ バージョン番号が異なる場合があります。
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
Codelab で説明されているバージョンではなく、常に最新バージョンのライブラリを使用することをおすすめします。
GameViewModel
という名前の新しい Kotlin クラスファイルを作成します。[Android] ウィンドウで ui.game フォルダを右クリックします。[New] > [Kotlin File/Class] を選択します。
- 名前を
GameViewModel
にして、リストから [Class] を選択します。 ViewModel
のサブクラスになるようにGameViewModel
を変更します。ViewModel
は抽象クラスであるため、アプリで使用するには拡張する必要があります。以下のGameViewModel
クラス定義をご覧ください。
class GameViewModel : ViewModel() {
}
ViewModel をフラグメントに接続する
ViewModel
を UI コントローラ(アクティビティ / フラグメント)に関連付けるために、UI コントローラ内に ViewModel
への参照(オブジェクト)を作成します。
このステップでは、対応する UI コントローラ(GameFragment
)内に GameViewModel
のオブジェクト インスタンスを作成します。
GameFragment
クラスの一番上に、GameViewModel
タイプのプロパティを追加します。by viewModels()
という Kotlin のプロパティ委譲を使用してGameViewModel
を初期化します。詳しくは次のセクションで説明します。
private val viewModel: GameViewModel by viewModels()
- Android Studio のプロンプトが表示された場合は、
androidx.fragment.app.viewModels
をインポートします。
Kotlin のプロパティ委譲
Kotlin では、可変(var
)プロパティに、デフォルトのゲッター関数とセッター関数が自動的に生成されます。セッター関数とゲッター関数は、値を代入するときと、プロパティの値を読み取るときに呼び出されます。
読み取り専用のプロパティ(val
)の場合は、可変プロパティとは少し異なります。デフォルトでは、ゲッター関数のみが生成されます。このゲッター関数は、読み取り専用のプロパティの値を読み取るときに呼び出されます。
Kotlin のプロパティ デリゲートにより、ゲッター / セッターの役割を別のクラスに引き継げます。
このクラス(委譲クラス)は、プロパティのゲッター関数とセッター関数を提供し、プロパティの変更を処理します。
委譲プロパティは、by
句と委譲クラスのインスタンスを使用して定義します。
// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
アプリで、次のようにデフォルトの GameViewModel
コンストラクタを使用してビューモデルを初期化すると、
private val viewModel = GameViewModel()
デバイスで構成変更が発生したとき、viewModel
参照の状態が失われます。たとえば、デバイスを回転させると、アクティビティは破棄されて、再度作成され、初期状態の新しいビューモデル インスタンスとなります。
代わりに、プロパティ デリゲートのアプローチを使用し、viewModel
オブジェクトの役割を viewModels
という名前の別のクラスに委任します。すると、viewModel
オブジェクトへのアクセスは、委譲クラス viewModels
によって内部的に処理されます。委譲クラスは、最初にアクセスされたときに viewModel
オブジェクトを作成し、構成変更後もその値を保持し、リクエストに対してその値を返します。
5. ViewModel にデータを移動する
アプリの UI データを UI コントローラ(Activity
クラスまたは Fragment
クラス)から分離すると、上記で説明した単一責任の原則に従いやすくなります。アクティビティとフラグメントには、ビューとデータを画面に描画する役割があります。一方で、ViewModel
は、UI に必要なすべてのデータを保持し、処理する役割を担います。
このタスクでは、データ変数を GameFragment
から GameViewModel
クラスに移動します。
- データ変数
score
、currentWordCount
、currentScrambledWord
をGameViewModel
クラスに移動します。
class GameViewModel : ViewModel() {
private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...
- 未解決の参照に関するエラーが発生します。これは、プロパティが
ViewModel
に対して非公開であり、UI コントローラからアクセスできないためです。このエラーを修正します。
この問題を解決するために、プロパティの可視性修飾子を public
にすることはできません。このデータを他のクラスから変更可能にすべきではありません。外部のクラスからこのデータに対して、ビューモデルで指定したゲームルールから外れた想定外の変更が行われる危険性があります。たとえば、外部のクラスが score
を負の値に変更する可能性が生じます。
ViewModel
内ではデータを編集できるように、private
かつ var
にする必要があります。ViewModel
の外部からは、データを読み取り可能にし、変更は不可能にする必要があるため、データを public
かつ val
として公開する必要があります。この動作を実現するため、Kotlin にはバッキング プロパティと呼ばれる機能があります。
バッキング プロパティ
バッキング プロパティを使用すると、そのオブジェクト自体ではなくゲッターから返すことができます。
Kotlin のフレームワークでは、どのプロパティにもゲッターとセッターが生成されることを学習しました。
ゲッター メソッドとセッター メソッドに関しては、両方または片方のメソッドをオーバーライドして、カスタムの動作を提供できます。バッキング プロパティを実装するには、ゲッター メソッドをオーバーライドして、データの読み取り専用バージョンを返すようにします。以下はバッキング プロパティの例です。
// Declare private mutable variable that can only be modified
// within the class it is declared.
private var _count = 0
// Declare another public immutable field and override its getter method.
// Return the private property's value in the getter method.
// When count is accessed, the get() function is called and
// the value of _count is returned.
val count: Int
get() = _count
例として、自分のアプリでアプリデータを ViewModel
に対して非公開にする場合を考えてみましょう。
ViewModel
クラスの内部:
- プロパティ
_count
は、private
かつ可変です。したがって、ViewModel
クラス内でのみアクセスと変更ができます。慣例では、private
プロパティの前にアンダースコアを付けます。
ViewModel
クラスの外部:
- Kotlin のデフォルトの可視性修飾子は
public
であるため、count
は公開され、UI コントローラなどの他のクラスからアクセス可能です。オーバーライドしようとしているのはget()
メソッドだけであるため、このプロパティは不変で読み取り専用です。外部のクラスがこのプロパティにアクセスすると、_count
の値が返されますが、その値は変更できません。これにより、ViewModel
内のアプリデータが、外部クラスによる望ましくない変更や安全でない変更から保護される一方で、外部の呼び出し元がその値に安全にアクセスできるようになります。
バッキング プロパティを currentScrambledWord に追加する
GameViewModel
でcurrentScrambledWord
宣言を変更して、バッキング プロパティを追加します。現在、_currentScrambledWord
はGameViewModel
内でのみアクセスと変更が可能です。UI コントローラであるGameFragment
は、読み取り専用のプロパティcurrentScrambledWord
を使用して、その値を読み取ることができます。
private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord
GameFragment
のメソッドupdateNextWordOnScreen()
を更新して、読み取り専用のviewModel
プロパティ(currentScrambledWord
)を使用するようにします。
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
GameFragment
のonSubmitWord()
メソッド内とonSkipWord()
メソッド内のコードを削除します。これらのメソッドは後で実装します。これで、コードをエラーなしでコンパイルできるようになるはずです。
6. ViewModel のライフサイクル
フレームワークは、アクティビティまたはフラグメントのスコープが有効である限り、ViewModel
を有効にします。ViewModel
は、画面の回転などの構成変更によってそのオーナーが破棄されても破棄されません。次の図のように、オーナーの新しいインスタンスが既存の ViewModel
インスタンスに再接続されます。
ViewModel のライフサイクルについて
GameViewModel
と GameFragment
にロギングを追加して、ViewModel
のライフサイクルについての理解を深めましょう。
GameViewModel.kt
に、ログ ステートメントを含むinit
ブロックを追加します。
class GameViewModel : ViewModel() {
init {
Log.d("GameFragment", "GameViewModel created!")
}
...
}
Kotlin には、オブジェクト インスタンスの初期化中に必要となる初期セットアップ コードの場所として、初期化ブロック(init
ブロック)が用意されています。初期化ブロックは、init
キーワードの後に波かっこ「{}
」が続くものです。このコードブロックは、オブジェクト インスタンスが最初に作成され初期化されたときに実行されます。
GameViewModel
クラスのonCleared()
メソッドをオーバーライドします。ViewModel
は、関連付けられたフラグメントの接続が解除されたとき、またはアクティビティが終了したときに破棄されます。ViewModel
が破棄される直前には、onCleared()
コールバックが呼び出されます。onCleared()
内にログ ステートメントを追加して、GameViewModel
のライフサイクルを追跡します。
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
onCreateView()
内のGameFragment
で、バインディング オブジェクトへの参照を取得し、フラグメントの作成を記録するログ ステートメントを追加します。onCreateView()
コールバックは、フラグメントが初めて作成されたとき、および構成変更などのイベントのために再作成されるたびにトリガーされます。
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = GameFragmentBinding.inflate(inflater, container, false)
Log.d("GameFragment", "GameFragment created/re-created!")
return binding.root
}
GameFragment
で、対応するアクティビティとフラグメントが破棄されたときに呼び出されるonDetach()
コールバック メソッドをオーバーライドします。
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
- Android Studio でアプリを実行し、[Logcat] ウィンドウを開き、
GameFragment
でフィルタします。GameFragment
とGameViewModel
が作成されているのがわかります。
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created!
- デバイスやエミュレータで自動回転設定を有効にして、画面の向きを数回変更します。
GameFragment
は毎回破棄されて再作成されますが、GameViewModel
は一度だけ作成され、呼び出しごとに再作成または破棄されることはありません。
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
- ゲームを終了するか、戻る矢印を使ってアプリから移動します。
GameViewModel
が破棄され、コールバックonCleared()
が呼び出されます。GameFragment
が破棄されます。
com.example.android.unscramble D/GameFragment: GameViewModel destroyed! com.example.android.unscramble D/GameFragment: GameFragment destroyed!
7. ViewModel にデータを入力する
このタスクでは、GameViewModel
に、次の単語を取得するヘルパー メソッド、プレーヤーの単語を検証してスコアを増やすヘルパー メソッド、単語カウントを確認してゲームを終了するヘルパー メソッドを追加します。
遅延初期化
通常、変数を宣言する場合、前もって初期値を指定します。ただし、値を代入する準備ができていない場合は、後で初期化できます。Kotlin でプロパティを後で初期化するには、lateinit
というキーワードを使用します。遅延初期化という意味です。プロパティの使用前に必ず初期化する場合は、lateinit
でプロパティを宣言できます。メモリは変数が初期化されるまで割り当てられません。初期化前に変数にアクセスしようとすると、アプリがクラッシュします。
次の単語を取得する
GameViewModel
クラスに、次の機能を備えた getNextWord()
メソッドを作成します。
allWordsList
からランダムな単語を取得し、currentWord.
に代入します。currentWord
内の文字を入れ替えてスクランブルされた単語を作成し、それをcurrentScrambledWord
に代入します。- スクランブルされた単語とスクランブルされていない単語が同じ場合に対処します。
- ゲーム中に同じ単語を 2 回表示しないようにします。
GameViewModel
クラスに以下のステップを実装します。
GameViewModel,
に、wordsList
という新しいMutableList<String>
型のクラス変数を追加し、ゲームで使用する単語のリストを保持して、重複を避けます。currentWord
という別のクラス変数を追加して、プレーヤーがスクランブル解除する単語を保持します。このプロパティは後で初期化するため、lateinit
キーワードを使用します。
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
- 戻り値もパラメータもない、
getNextWord()
という新しいprivate
メソッドをinit
ブロックの上に追加します。 allWordsList
からランダムな単語を取得して、currentWord
に代入します。
private fun getNextWord() {
currentWord = allWordsList.random()
}
getNextWord()
で、currentWord
文字列を文字の配列に変換し、tempWord
という新しいval
に代入します。単語をスクランブルするために、Kotlin メソッドshuffle()
を使用して、この配列内の文字をシャッフルします。
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
Array
は MutableList
に似ていますが、初期化時にサイズが固定されます。Array
では、サイズの増減はできません(配列のサイズを変更するにはコピーする必要があります)。これに対して、MutableList
には add()
関数と remove()
関数があり、サイズを増減できます。
- シャッフルした文字の順序が、元の単語と同じになる場合もあります。次のように shuffle への呼び出しの周りに
while
を追加して、スクランブルされた単語が元の単語と同じである限りループし続けるようにします。
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
if-else
ブロックを追加して、単語がすでに使用されているかどうかを確認します。wordsList
にcurrentWord
が含まれている場合は、getNextWord()
を呼び出します。それ以外の場合は、新たにスクランブルされた単語で_currentScrambledWord
の値を更新し、単語カウントを増やして、wordsList
に新しい単語を追加します。
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++currentWordCount
wordsList.add(currentWord)
}
- 完成した
getNextWord()
メソッドを以下に示します。
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
currentWord = allWordsList.random()
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++currentWordCount
wordsList.add(currentWord)
}
}
currentScrambledWord を遅延初期化する
これで、次のスクランブルされた単語を取得する getNextWord()
メソッドが作成されました。初めて GameViewModel
が初期化されたときに、これを呼び出します。init
ブロックを使用して、現在の単語などのクラス内の lateinit
プロパティを初期化します。その結果、画面に表示される最初の単語が「test」ではなくスクランブルされた単語になります。
- アプリを実行します。最初の単語は常に「test」なのがわかります。
- アプリの開始時にスクランブルされた単語を表示するには、
getNextWord()
メソッドを呼び出す必要があります。その後、currentScrambledWord
が更新されます。GameViewModel
のinit
ブロック内でメソッドgetNextWord()
を呼び出します。
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
lateinit
修飾子を_currentScrambledWord
プロパティに追加します。初期値が指定されていないため、データ型String
を明示的に指定します。
private lateinit var _currentScrambledWord: String
- アプリを実行します。起動すると、スクランブルされた新しい単語が表示されます。うまくできました。
ヘルパー メソッドを追加する
次に、ViewModel
内のデータを処理して変更するヘルパー メソッドを追加します。このメソッドは後のタスクで使用します。
GameViewModel
クラスに、nextWord().
という別のメソッドを追加します。リストから次の単語を取得し、単語カウントがMAX_NO_OF_WORDS
より小さい場合にはtrue
を返します。
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS.
* Updates the next word.
*/
fun nextWord(): Boolean {
return if (currentWordCount < MAX_NO_OF_WORDS) {
getNextWord()
true
} else false
}
8. ダイアログ
スターター コードでは、10 単語をプレイしてもゲームが終了しません。単語が 10 個を超えたらゲームを終わりにして、最終スコアのダイアログを表示するようにします。また、もう一度プレイするかゲームを終了するかを選択できるようにもします。
アプリにダイアログを追加するのは、これが初めてになります。ダイアログは、ユーザーによる意思決定や追加情報の入力を求める小さなウィンドウ(画面)です。通常、ダイアログは画面全体に表示されるのではなく、また続行するにはユーザーが操作を行う必要があります。Android には、さまざまな種類のダイアログが用意されています。この Codelab では、アラート ダイアログについて学習します。
アラート ダイアログの構造
- アラート ダイアログ
- タイトル(省略可)
- メッセージ
- テキストボタン
最終スコアのダイアログを実装する
マテリアル デザイン コンポーネント ライブラリの MaterialAlertDialog
を使用すると、マテリアル ガイドラインを遵守したダイアログをアプリに追加できます。ダイアログは UI に関連しているため、GameFragment
が最終スコアのダイアログを作成して表示する役割を担います。
- まず、バッキング プロパティを
score
変数に追加します。GameViewModel
で、score
変数の宣言を次のように変更します。
private var _score = 0
val score: Int
get() = _score
GameFragment
に、showFinalScoreDialog()
という非公開関数を追加します。MaterialAlertDialog
を作成するために、MaterialAlertDialogBuilder
クラスを使ってダイアログの構成要素を段階的に構築します。MaterialAlertDialogBuilder
コンストラクタを呼び出して、フラグメントのrequireContext()
メソッドを使用して内容を渡します。requireContext()
メソッドは null 以外のContext
を返します。
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
}
Context
は、その名前が示すように、アプリケーション、アクティビティ、フラグメントのコンテキストまたは現在の状態を表します。これには、アクティビティ、フラグメント、またはアプリケーションに関する情報が含まれています。通常、リソース、データベース、その他のシステム サービスへのアクセスに使用されます。このステップでは、フラグメントのコンテキストを渡してアラート ダイアログを作成します。
Android Studio でプロンプトが表示されたら、com.google.android.material.dialog.MaterialAlertDialogBuilder
を import
します。
- アラート ダイアログにタイトルを設定するコードを追加します。
strings.xml
の文字列リソースを使用します。
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
- 最終スコアを示すメッセージを設定します。先ほど追加したスコア変数(
viewModel.score
)の読み取り専用バージョンを使用します。
.setMessage(getString(R.string.you_scored, viewModel.score))
- [戻る] キーが押されてもアラート ダイアログがキャンセルされないように、
setCancelable()
メソッドを使用してfalse
を渡します。
.setCancelable(false)
- メソッド
setNegativeButton()
とsetPositiveButton()
を使用して、2 つのテキストボタン [EXIT] と [PLAY AGAIN] を追加します。exitGame()
とrestartGame()
をそれぞれのラムダから呼び出します。
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
この構文はまだ説明していないかもしれませんが、setNegativeButton(getString(R.string.exit), { _, _ -> exitGame()})
の略となっています。ここで、setNegativeButton()
メソッドは、String
と、ラムダとして記述できる関数 DialogInterface.OnClickListener()
の 2 つのパラメータを取ります。渡す引数の最後が関数である場合は、ラムダ式をかっこの外側に置くことができます。これは後置ラムダ構文と呼ばれます。どちらのコード記述方法も(ラムダをかっこ内に書いても、かっこ外に書いても)許容されます。setPositiveButton
関数についても同様です。
- 最後に、アラート ダイアログを作成してから表示する
show()
を追加します。
.show()
- 参考のため
showFinalScoreDialog()
メソッド全体を次に示します。
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score))
.setCancelable(false)
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
.show()
}
9. Submit ボタンの OnClickListener を実装する
このタスクでは、ViewModel
と追加したアラート ダイアログを使って、[Submit] ボタンのクリック リスナー用のゲームロジックを実装します。
スクランブルされた単語を表示する
- まだ行っていない場合は、
GameFragment
で、[Submit] ボタンがタップされたときに呼び出されるonSubmitWord()
内のコードを削除します。 viewModel.nextWord()
メソッドの戻り値のチェックを追加します。true
の場合、別の単語があるので、updateNextWordOnScreen()
を使用して、画面上のスクランブルされた単語を更新します。それ以外の場合は、ゲームが終了し、最終スコアとともにアラート ダイアログが表示されます。
private fun onSubmitWord() {
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
- アプリを実行しましょう。単語数個分をプレイします。[Skip] ボタンが実装されていないため、単語のスキップはできません。
- テキスト フィールドは更新されないため、プレーヤーが前の単語を手動で削除する必要があります。アラート ダイアログの最終スコアは常に 0 です。これらのバグは、次のステップで修正します。
プレーヤーの単語を検証するヘルパー メソッドを追加する
GameViewModel
に、パラメータも戻り値もないincreaseScore()
という新しい非公開メソッドを追加します。score
変数をSCORE_INCREASE
の分だけ増やします。
private fun increaseScore() {
_score += SCORE_INCREASE
}
GameViewModel
にisUserWordCorrect()
というヘルパー メソッドを追加します。このメソッドはBoolean
を返し、プレーヤーの単語であるString
をパラメータとして受け取ります。isUserWordCorrect()
で、プレーヤーの単語を検証し、推測が正しい場合はスコアを増やします。これにより、アラート ダイアログの最終スコアが更新されます。
fun isUserWordCorrect(playerWord: String): Boolean {
if (playerWord.equals(currentWord, true)) {
increaseScore()
return true
}
return false
}
テキスト フィールドを更新する
テキスト フィールドにエラーを表示する
マテリアル テキスト フィールドの場合、TextInputLayout
にはエラー メッセージを表示する機能が組み込まれています。たとえば、次のテキスト フィールドでは、ラベルの色の変更、エラーアイコンの表示、エラー メッセージの表示などが行われています。
テキスト フィールドにエラーを表示するには、エラー メッセージの設定をコード内で動的に行うか、レイアウト ファイル内で静的に行います。コード内でエラーの設定と解除を行うコードは、次のとおりです。
// Set error text
passwordLayout.error = getString(R.string.error)
// Clear error text
passwordLayout.error = null
このスターター コードにあるヘルパー メソッド setErrorTextField(error: Boolean)
はすでに、テキスト フィールドに対するエラーの設定と解除に使用できるように定義されています。テキスト フィールドにエラーを表示するかどうかに応じて、入力パラメータに true
または false
を指定してこのメソッドを呼び出します。
スターター コード内のコード スニペット
private fun setErrorTextField(error: Boolean) {
if (error) {
binding.textField.isErrorEnabled = true
binding.textField.error = getString(R.string.try_again)
} else {
binding.textField.isErrorEnabled = false
binding.textInputEditText.text = null
}
}
このタスクでは、onSubmitWord()
メソッドを実装します。単語が送信されたら、元の単語とユーザーの推測を照合します。単語が正しい場合は、次の単語に進みます(ゲームが終了した場合はダイアログを表示します)。単語が間違っている場合は、テキスト フィールドにエラーを表示し、現在の単語はそのままにします。
GameFragment,
のonSubmitWord()
の先頭で、playerWord
という名前のval
を作成します。そこに、binding
変数のテキスト フィールドから抽出したプレーヤーの単語を保存します。
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
...
}
onSubmitWord()
のplayerWord
の宣言の下で、プレーヤーの単語を検証します。プレーヤーの単語を確認するif
ステートメントを追加して、isUserWordCorrect()
メソッドを使用してplayerWord
を渡します。if
ブロック内でテキスト フィールドをリセットし、setErrorTextField
を呼び出してfalse
を渡します。- 既存のコードを
if
ブロック内に移動します。
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
}
- ユーザーの単語が間違っている場合は、テキスト フィールドにエラー メッセージを表示します。さらに、上の
if
ブロックにelse
ブロックを追加し、setErrorTextField()
を呼び出してtrue
に渡します。完成したonSubmitWord()
メソッドは次のようになります。
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
} else {
setErrorTextField(true)
}
}
- アプリを実行して、数語プレイします。プレーヤーの単語が正しい場合は、[Submit] ボタンをクリックするとその単語がクリアされ、正しくない場合は「Try again!」というメッセージが表示されます。なお、[Skip] ボタンはまだ使用できません。この実装は次のタスクで追加します。
10. [Skip] ボタンを実装する
このタスクでは、[Skip] ボタンのクリックを処理する onSkipWord()
の実装を追加します。
onSubmitWord()
と同様に、onSkipWord()
メソッドに条件を追加します。true
の場合は、画面に単語を表示し、テキスト フィールドをリセットします。false
の場合は、このゲームに残りの単語がない場合、最終スコアをアラート ダイアログに表示します。
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
if (viewModel.nextWord()) {
setErrorTextField(false)
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
- アプリを実行して、ゲームをプレイします。[Skip] ボタンと [Submit] ボタンが意図したとおりに動作しています。うまくできました。
11. ViewModel がデータを維持することを確認する
このタスクでは、GameFragment
にロギングを追加して、構成変更後もアプリデータが ViewModel
に維持されていることを確認します。GameFragment
の currentWordCount
にアクセスするには、バッキング プロパティを使用して読み取り専用バージョンを公開する必要があります。
GameViewModel
で、変数currentWordCount
を右クリックし、[Refactor] > [Rename...] を選択します。新しい名前の先頭にアンダースコアを付けます(_currentWordCount
)。- バッキング フィールドを追加します。
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
GameFragment
のonCreateView()
内で、return 文の上にアプリデータ、単語、スコア、単語カウントを出力するログを追加します。
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
"Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
- Android Studio で [Logcat] を開き、
GameFragment
でフィルタします。アプリを実行して、数語プレイします。デバイスの向きを変更します。フラグメント(UI コントローラ)が破棄されて再作成されます。ログを確認します。スコアと単語カウントが増えたことがわかります。
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created! com.example.android.unscramble D/GameFragment: Word: oimfnru Score: 0 WordCount: 1 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9
向きを変更した後もアプリデータが ViewModel
に維持されています。後の Codelab では、LiveData
とデータ バインディングを使用して、UI のスコア値と単語カウントを更新します。
12. ゲームの再プレイのロジックを更新する
- アプリを再度実行し、すべての単語をプレイします。[Congratulations!] アラート ダイアログで [PLAY AGAIN] をクリックします。単語カウントが
MAX_NO_OF_WORDS
に達したため、もう一度ゲームをプレイすることはできません。最初からゲームをプレイするには、単語カウントを 0 にリセットする必要があります。 - アプリデータをリセットするために、
GameViewModel
にreinitializeData()
というメソッドを追加します。スコアと単語カウントを0
に設定します。単語リストをクリアしてgetNextWord()
メソッドを呼び出します。
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
GameFragment
のメソッドrestartGame()
の先頭で、新しく作成したメソッドreinitializeData()
を呼び出します。
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
- アプリを再度実行します。ゲームをプレイします。[Congratulations!] ダイアログが表示されたら、[Play Again] をクリックします。ゲームをもう一度プレイできるようになりました。
最終的なアプリは次のように表示されます。このゲームでは、プレーヤーがスクランブル解除するランダムな単語を 10 個表示します。単語を [Skip] するか、推測して、[Submit] をタップできます。正解するとスコアが増えます。回答が間違っていると、テキスト フィールドにエラー状態が表示されます。単語が新しくなるたびに、単語カウントも増えます。
なお、画面に表示されるスコアと文字数は、まだ更新されません。しかし、この情報はビューモデルに格納されていて、デバイスの回転などの構成変更後も維持されます。後の Codelab で、画面上のスコアと単語カウントを更新します。
最後の 10 個目の単語でゲームが終了し、最終スコアとゲームを終了するかもう一度プレイするかを選ぶ選択肢があるアラート ダイアログが表示されます。
これで完成です。最初の ViewModel
を作成してデータを保存しました。
13. 解答コード
GameFragment.kt
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.android.unscramble.R
import com.example.android.unscramble.databinding.GameFragmentBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* Fragment where the game is played, contains the game logic.
*/
class GameFragment : Fragment() {
private val viewModel: GameViewModel by viewModels()
// Binding object instance with access to the views in the game_fragment.xml layout
private lateinit var binding: GameFragmentBinding
// Create a ViewModel the first time the fragment is created.
// If the fragment is re-created, it receives the same GameViewModel instance created by the
// first fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout XML file and return a binding object instance
binding = GameFragmentBinding.inflate(inflater, container, false)
Log.d("GameFragment", "GameFragment created/re-created!")
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
"Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Setup a click listener for the Submit and Skip buttons.
binding.submit.setOnClickListener { onSubmitWord() }
binding.skip.setOnClickListener { onSkipWord() }
// Update the UI
updateNextWordOnScreen()
binding.score.text = getString(R.string.score, 0)
binding.wordCount.text = getString(
R.string.word_count, 0, MAX_NO_OF_WORDS)
}
/*
* Checks the user's word, and updates the score accordingly.
* Displays the next scrambled word.
* After the last word, the user is shown a Dialog with the final score.
*/
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
} else {
setErrorTextField(true)
}
}
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
if (viewModel.nextWord()) {
setErrorTextField(false)
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
/*
* Gets a random word for the list of words and shuffles the letters in it.
*/
private fun getNextScrambledWord(): String {
val tempWord = allWordsList.random().toCharArray()
tempWord.shuffle()
return String(tempWord)
}
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score))
.setCancelable(false)
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
.show()
}
/*
* Re-initializes the data in the ViewModel and updates the views with the new data, to
* restart the game.
*/
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
/*
* Exits the game.
*/
private fun exitGame() {
activity?.finish()
}
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
/*
* Sets and resets the text field error status.
*/
private fun setErrorTextField(error: Boolean) {
if (error) {
binding.textField.isErrorEnabled = true
binding.textField.error = getString(R.string.try_again)
} else {
binding.textField.isErrorEnabled = false
binding.textInputEditText.text = null
}
}
/*
* Displays the next scrambled word on screen.
*/
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
}
GameViewModel.kt
import android.util.Log
import androidx.lifecycle.ViewModel
/**
* ViewModel containing the app data and methods to process the data
*/
class GameViewModel : ViewModel(){
private var _score = 0
val score: Int
get() = _score
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
private lateinit var _currentScrambledWord: String
val currentScrambledWord: String
get() = _currentScrambledWord
// List of words used in the game
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
currentWord = allWordsList.random()
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++_currentWordCount
wordsList.add(currentWord)
}
}
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
/*
* Increases the game score if the player's word is correct.
*/
private fun increaseScore() {
_score += SCORE_INCREASE
}
/*
* Returns true if the player word is correct.
* Increases the score accordingly.
*/
fun isUserWordCorrect(playerWord: String): Boolean {
if (playerWord.equals(currentWord, true)) {
increaseScore()
return true
}
return false
}
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS
*/
fun nextWord(): Boolean {
return if (_currentWordCount < MAX_NO_OF_WORDS) {
getNextWord()
true
} else false
}
}
14. まとめ
- Android アプリのアーキテクチャ ガイドラインでは、異なる役割を持つクラスに分割し、モデルで UI を操作することを推奨しています。
- UI コントローラは、
Activity
やFragment
などの UI ベースのクラスです。UI コントローラには、UI やオペレーティング システムとのやり取りを処理するロジックのみを含めるべきです。UI に表示するデータのソースにするべきではありません。そのようなデータと関連ロジックはViewModel
に入れてください。 ViewModel
クラスは、UI 関連のデータを保存し管理します。ViewModel
クラスを使用すると、画面の回転などの構成変更後もデータを維持することができます。ViewModel
は、推奨される Android アーキテクチャ コンポーネントの 1 つです。