ViewModel の概要   Android Jetpack の一部

Kotlin Multiplatform で試す
Kotlin Multiplatform を使用すると、ビジネス ロジックを他のプラットフォームと共有できます。KMP で ViewModel を設定して使用する方法について説明します

ViewModel クラスは、ビジネス ロジックまたは画面レベルの状態 ホルダーです。状態を UI に公開し、関連するビジネス ロジックをカプセル化します。状態がキャッシュに保存され、構成が変更されてもそれが維持されることが主なメリットです。つまり、アクティビティ間を移動するときや、画面の回転などの構成の変更に従うときに、UI でデータを再度取得する必要がありません。

状態ホルダーの詳細については、状態ホルダーのガイダンスをご覧ください。また、UI レイヤの一般的情報については、UI レイヤのガイダンスをご覧ください。

ViewModel のメリット

ViewModel の代わりに、UI に表示するデータを保持するプレーンクラスを利用できます。これは、アクティビティ間や Navigation デスティネーション間を移動する際に問題になることがあります。インスタンス状態保存メカニズムを使用して保存せずにこれを行うと、そのデータが破棄されます。ViewModel であれば、データの永続性のための便利な API を利用して、この問題を解決できます。

または、純粋な状態ホルダーの場合、Compose には retain 機能があり、ViewModel の完全なインフラストラクチャを使用せずに、プレーンクラスが構成の変更後も存続できるようにします。どちらのメカニズムも状態の保持に役立ちますが、ライフサイクルとクリーンアップ動作が異なるため、一般的には、保持されたインスタンスに ViewModel を提供する方が安全です。

ViewModel クラスの主なメリットは基本的に次の 2 つです。

  • UI 状態を保持できます。
  • ビジネス ロジックにアクセスできます。

永続性

ViewModel は、ViewModel で保持される状態と、ViewModel でトリガーされるオペレーションの両方で、永続性を実現します。このキャッシュにより、画面回転などの一般的な構成の変更で、データを再度取得する必要がなくなります。

範囲

ViewModel をインスタンス化する場合、 ViewModelStoreOwner インターフェースを実装するオブジェクトを渡します。これは、Navigation デスティネーション、Navigation グラフ、アクティビティ、その他のインターフェースを実装するなんらかのタイプになります。`rememberViewModelStoreOwner` API を使用して、ViewModel をコンポーザブルに直接スコープすることもできます。これにより、ライフサイクルViewModelStoreOwnerの ViewModel のスコープが設定されます。これは、ViewModelStoreOwner が完全に削除されるまで(コンポーザブル オーナーがコンポジションを終了したときなど)メモリ内に残ります。

クラスの範囲は、ViewModelStoreOwner インターフェースの直接または間接のサブクラスです。直接のサブクラスは ComponentActivityNavBackStackEntry です。 間接サブクラスの完全なリストについては、 ViewModelStoreOwner リファレンスをご覧ください。ViewModel のスコープを LazyList または Pager の個々のアイテムに設定するには、rememberViewModelStoreProvider() を使用して、オーナー管理を親にホイストします。

ホスト アクティビティで構成が変更されると、ViewModel がアクティビティまたは特定のコンポーザブルにスコープされているかどうかにかかわらず、ViewModel で非同期処理が続行されます。これが永続性の鍵となります。

詳細については、ViewModel のライフサイクルに関する以下のセクション、 ViewModel スコープ設定 API、 Jetpack Compose の状態ホイスティングに関するガイドをご覧ください。

SavedStateHandle

SavedStateHandle を使用すると、構成 の変更でのみならず、プロセスの終了後もデータを保持できます。つまり、ユーザーがアプリを閉じてから後で開いた場合でも、UI の状態を維持できます。

UI の状態の保存について詳しくは、 Compose で UI の状態を保存するをご覧ください。

ビジネス ロジックへのアクセス

ビジネス ロジックの大部分はデータレイヤに存在しますが、UI レイヤにビジネス ロジックも含めることができます。これは、複数のリポジトリからのデータを組み合わせて画面 UI の状態を作成する場合や、特定の種類のデータにデータレイヤが不要な場合などに該当します。

ViewModel は、UI レイヤでビジネス ロジックを処理するのに適した場所です。また、アプリデータの変更のためにビジネス ロジックを適用する必要がある場合、ViewModel はイベントの処理も担い、階層の他のレイヤに委任します。

ViewModel を実装する

ユーザーがサイコロを振ることができる画面の ViewModel の実装例を次に示します。

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,
            )
        }
    }
}

次のように、ViewModel には画面レベルのコンポーザブルからアクセスできます。

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 エントリの場合はバックスタックからの削除時。
  • コンポーザブルの場合はコンポジションの終了時。rememberViewModelStoreOwner を使用すると、ViewModel のスコープを UI の任意のパーツ(PagerLazyList など)に直接設定できます。

これにより、ViewModel は、構成の変更後に引き継ぐデータを保存するための優れたソリューションとなっています。

図 1 に、回転を経て終了するまでのアクティビティのさまざまなライフサイクルの状態を示します。この図には、関連するアクティビティのライフサイクルの横に ViewModelのライフタイムも示されています。この図はアクティビティの状態を示しています。

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 コンポーネント(チップグループやフォームなど)の状態ホルダーとしては使用しないでください。それ以外の場合は、チップごとに明示的なビューモデルキーを使用しない限り、同じ ViewModelStoreOwner の下で、同じ UI コンポーネントの異なる用途で同じ ViewModel インスタンスを取得します。
  • ViewModel が、UI 実装の詳細を認識しないようにします。ViewModel API が公開するメソッドの名前と、UI の状態フィールドの名前は、できるだけ汎用的なものにしてください。このようにすることで、ViewModel があらゆる種類の UI(スマートフォン、折りたたみ式デバイス、タブレット、Chromebook)に対応できるようになります。
  • ViewModel は ViewModelStoreOwner よりも長く存続する可能性があるため、メモリリークを防ぐためにライフサイクル関連の API(ContextResources など)の参照を保持しないようにします。
  • ViewModel を他のクラス、関数、その他の UI コンポーネントに渡さないようにします。 プラットフォームで管理するため、可能な限りプラットフォームの近くに置く必要があります。Activity、画面レベルのコンポーズ可能な関数、Navigation デスティネーションの近くです。これにより、下位レベルのコンポーネントが必要以上にデータやロジックにアクセスすることを防止できます。

追加情報

データの複雑さが増すと、データの読み込みのためだけに別のクラスを使用することがあります。ViewModel の目的は、UI コントローラのデータをカプセル化して、構成の変更後にもデータが引き継がれるようにすることです。構成の変更の前後におけるデータの読み込み、永続化、管理の方法については、保存された UI の状態をご覧ください。

Android アプリのアーキテクチャ ガイドでは、これらの機能を処理するためにリポジトリ クラスを作成することが推奨されています。

参考情報

ViewModel クラスについて詳しくは、以下のリソースをご覧ください。

ドキュメント

ビューのコンテンツ

サンプル