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 インターフェースの直接または間接のサブクラスです。直接サブクラスは、ComponentActivityFragmentNavBackStackEntry です。間接サブクラスの完全なリストについては、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 のライフタイムも示されています。この図はアクティビティの状態を示していますが、フラグメントのライフサイクルの状態も基本的には同じです。

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 コンポーネントに渡さないようにします。プラットフォームで管理するため、可能な限りプラットフォームの近くに置く必要があります。アクティビティ、フラグメント、画面レベルのコンポーズ可能な関数の近くです。これにより、下位レベルのコンポーネントが必要以上にデータやロジックにアクセスすることを防止できます。

追加情報

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

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

参考情報

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

ドキュメント

サンプル