ViewModel の概要 Android Jetpack の一部。
ViewModel
クラスは、ビジネス ロジックまたは画面レベルの状態ホルダーです。状態を UI に公開し、関連するビジネス ロジックをカプセル化します。状態がキャッシュに保存され、構成が変更されてもそれが維持されることが主なメリットです。つまり、アクティビティ間を移動するときや、画面の回転などの構成の変更に従うときに、UI でデータを再度取得する必要がありません。
状態ホルダーの詳細については、状態ホルダーのガイダンスをご覧ください。また、UI レイヤの一般的情報については、UI レイヤのガイダンスをご覧ください。
ViewModel のメリット
ViewModel の代わりに、UI に表示するデータを保持するプレーンクラスを利用できます。これは、アクティビティ間や Navigation デスティネーション間を移動する際に問題になることがあります。インスタンス状態保存メカニズムを使用して保存せずにこれを行うと、そのデータが破棄されます。ViewModel であれば、データの永続性のための便利な API を利用して、この問題を解決できます。
ViewModel クラスの主なメリットは基本的に次の 2 つです。
- UI 状態を保持できます。
- ビジネス ロジックにアクセスできます。
永続性
ViewModel は、ViewModel で保持される状態と、ViewModel でトリガーされるオペレーションの両方で、永続性を実現します。このようにキャッシュに保存されることで、一般的な構成の変更(画面回転など)があってもデータを再度取得する必要がなくなります。
範囲
ViewModel をインスタンス化する場合、ViewModelStoreOwner
インターフェースを実装するオブジェクトを渡します。これは、Navigation デスティネーション、Navigation グラフ、アクティビティ、フラグメント、その他のインターフェースを実装するなんらかのタイプになります。これにより、ViewModelStoreOwner
のライフサイクルに ViewModel のスコープが設定されます。これは、ViewModelStoreOwner
が完全に削除されるまでメモリ内に残ります。
クラスの範囲は、ViewModelStoreOwner
インターフェースの直接または間接のサブクラスです。直接サブクラスは、ComponentActivity
、Fragment
、NavBackStackEntry
です。間接サブクラスの完全なリストについては、ViewModelStoreOwner
リファレンスをご覧ください。
ViewModel がスコープされているフラグメントまたはアクティビティが破棄されると、スコープされている ViewModel では非同期処理が続行されます。これが永続性の鍵となります。
詳しくは、ViewModel のライフサイクルに関する以下のセクションをご覧ください。
SavedStateHandle
SavedStateHandle を使用すると、構成の変更でのみならず、プロセスの再作成後もデータを保持できます。つまり、ユーザーがアプリを閉じてから後で開いた場合でも、UI の状態を維持できます。
ビジネス ロジックへのアクセス
ビジネス ロジックの大部分はデータレイヤに存在しますが、UI レイヤにビジネス ロジックも含めることができます。これは、複数のリポジトリからのデータを組み合わせて画面 UI の状態を作成する場合や、特定の種類のデータにデータレイヤが不要な場合などに該当します。
ViewModel は、UI レイヤでビジネス ロジックを処理するのに適した場所です。また、アプリデータの変更のためにビジネス ロジックを適用する必要がある場合、ViewModel はイベントの処理も担い、階層の他のレイヤに委任します。
Jetpack Compose
Jetpack Compose を使用する場合、ViewModel は、コンポーザブルに画面 UI の状態を公開する主要な手段です。ハイブリッド アプリの場合、アクティビティとフラグメントでコンポーズ可能な関数をホストするだけです。これは、アクティビティやフラグメントで再利用可能な UI を作成することがそれほど単純で直感的ではなかった以前のアプローチからシフトしたものであり、UI コントローラとしてはるかに有効になりました。
Compose で ViewModel を使用する場合に理解すべき最も重要なことは、ViewModel のスコープをコンポーザブルに設定できないことです。これは、コンポーザブルが ViewModelStoreOwner
ではないためです。コンポジション内の同じコンポーザブルの 2 つのインスタンス、または同じ ViewModelStoreOwner
の同じ ViewModel タイプにアクセスする 2 つの異なるコンポーザブルは、ViewModel の同じインスタンスを受け取ります。通常は、このような動作は想定されたものではありません。
Compose で ViewModel からメリットを得るには、各画面をフラグメントまたはアクティビティでホストするか、Compose Navigation を使用して、Navigation デスティネーションにできるだけ近いコンポーズ可能な関数で ViewModel を使用します。これは、ViewModel のスコープを Navigation デスティネーション、Navigation グラフ、アクティビティ、フラグメントに設定できるためです。
詳しくは、Jetpack Compose の状態ホイスティングに関するガイドをご覧ください。
ViewModel を実装する
ユーザーがサイコロを振ることができる画面の ViewModel の実装例を次に示します。
Kotlin
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
// Expose screen UI state
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Handle business logic
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Java
public class DiceUiState {
private final Integer firstDieValue;
private final Integer secondDieValue;
private final int numberOfRolls;
// ...
}
public class DiceRollViewModel extends ViewModel {
private final MutableLiveData<DiceUiState> uiState =
new MutableLiveData(new DiceUiState(null, null, 0));
public LiveData<DiceUiState> getUiState() {
return uiState;
}
public void rollDice() {
Random random = new Random();
uiState.setValue(
new DiceUiState(
random.nextInt(7) + 1,
random.nextInt(7) + 1,
uiState.getValue().getNumberOfRolls() + 1
)
);
}
}
次のように、ViewModel にはアクティビティからアクセスできます。
Kotlin
import androidx.activity.viewModels
class DiceRollActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same DiceRollViewModel instance created by the first activity.
// Use the 'by viewModels()' Kotlin property delegate
// from the activity-ktx artifact
val viewModel: DiceRollViewModel by viewModels()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
Java
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
model.getUiState().observe(this, uiState -> {
// update UI
});
}
}
Jetpack Compose
import androidx.lifecycle.viewmodel.compose.viewModel
// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
viewModel: DiceRollViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Update UI elements
}
ViewModel でコルーチンを使用する
ViewModel
には、Kotlin コルーチンのサポートが含まれています。UI 状態を永続化する場合と同じ方法で、非同期処理を永続化できます。
詳しくは、Android アーキテクチャ コンポーネントで Kotlin コルーチンを使用するをご覧ください。
ViewModel のライフサイクル
ViewModel
のライフサイクルは、そのスコープに直接関連付けられます。ViewModel
は、スコープに設定されている ViewModelStoreOwner
が消えるまでメモリ内にとどまります。これは、次のような状況で発生します。
- アクティビティの場合は終了時。
- フラグメントの場合はデタッチ時。
- Navigation エントリの場合はバックスタックからの削除時。
これにより、ViewModel は、構成の変更後に引き継ぐデータを保存するための優れたソリューションとなっています。
図 1 に、回転を経て終了するまでのアクティビティのさまざまなライフサイクルの状態を示します。この図には、関連するアクティビティのライフサイクルの横に ViewModel
のライフタイムも示されています。この図はアクティビティの状態を示していますが、フラグメントのライフサイクルの状態も基本的には同じです。
通常は、アクティビティ オブジェクトの onCreate()
メソッドが最初に呼び出されたときに、ViewModel
をリクエストします。onCreate()
は、デバイスの画面が回転されたときなど、アクティビティの存続期間全体を通して複数回呼び出されることがあります。ViewModel
は、
まず、アクティビティが終了して破棄されるまで、ViewModel
をリクエストします。
ViewModel の依存関係をクリアする
ViewModel は、ライフサイクル中に ViewModelStoreOwner
によって破棄されると、onCleared
メソッドを呼び出します。これにより、ViewModel のライフサイクルに従う処理や依存関係をクリーンアップできます。
次の例は、viewModelScope
に代わるものです。viewModelScope
は、ViewModel のライフサイクルに自動的に従う、組み込みの CoroutineScope
です。これを使用して、ViewModel はビジネス関連のオペレーションをトリガーします。テストを容易にするために viewModelScope
ではなくカスタム スコープを使用する場合、ViewModel はコンストラクタの依存関係として CoroutineScope
を受け取ることができます。ViewModelStoreOwner
がライフサイクルの終了時に ViewModel をクリアすると、ViewModel も CoroutineScope
をキャンセルします。
class MyViewModel(
private val coroutineScope: CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {
// Other ViewModel logic ...
override fun onCleared() {
coroutineScope.cancel()
}
}
ライフサイクル バージョン 2.5 以降では、ViewModel インスタンスがクリアされると自動的に終了する ViewModel のコンストラクタに 1 つ以上の Closeable
オブジェクトを渡すことができます。
class CloseableCoroutineScope(
context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
class MyViewModel(
private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
// Other ViewModel logic ...
}
ベスト プラクティス
ViewModel を実装する際は、次の主要なベスト プラクティスを行う必要があります。
- スコープ設定のため、ViewModel を画面レベルの状態ホルダーの実装の詳細として使用します。再利用可能な UI コンポーネント(チップグループやフォームなど)の状態ホルダーとしては使用しないでください。そうでない場合は、 同じ UI コンポーネントの異なる用途での ViewModel インスタンス ViewModelStoreOwner(チップごとに明示的なビューモデルキーを使用する場合を除きます)。
- ViewModel が、UI 実装の詳細を認識しないようにします。ViewModel API が公開するメソッドの名前と、UI の状態フィールドの名前は、できるだけ汎用的なものにしてください。このようにすることで、ViewModel があらゆる種類の UI(スマートフォン、折りたたみ式デバイス、タブレット、Chromebook)に対応できるようになります。
- ViewModel は
ViewModelStoreOwner
よりも長く存続する可能性があるため、 ライフサイクル関連の API(Context
など)への参照を保持しないようにする必要があります。 またはResources
を使用してメモリリークを防ぎます。 - ViewModel を他のクラス、関数、その他の UI コンポーネントに渡さないようにします。プラットフォームで管理するため、可能な限りプラットフォームの近くに置く必要があります。アクティビティ、フラグメント、画面レベルのコンポーズ可能な関数の近くです。これにより、下位レベルのコンポーネントが必要以上にデータやロジックにアクセスすることを防止できます。
追加情報
データの複雑さが増すと、データの読み込みのためだけに別のクラスを使用することがあります。ViewModel
の目的は、UI コントローラのデータをカプセル化して、構成の変更後にもデータが引き継がれるようにすることです。構成の変更の前後におけるデータの読み込み、永続化、管理の方法については、保存された UI の状態をご覧ください。
Android アプリのアーキテクチャ ガイドでは、これらの機能を処理するためにリポジトリ クラスを作成することが推奨されています。
参考情報
ViewModel
クラスについて詳しくは、以下のリソースをご覧ください。
ドキュメント
サンプル
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- ライフサイクル対応コンポーネントで Kotlin コルーチンを使用する
- UI の状態を保存する
- ページング データを読み込む、表示する