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

SaveStateHandle を使用すると、構成の変更でのみならず、プロセスの再作成後もデータを保持できます。つまり、ユーザーがアプリを閉じてから後で開いた場合でも、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 MyViewModel extends ViewModel {

    // Expose screen UI state
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }

    // Handle business logic
    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

次のように、リストにはアクティビティからアクセスできます。

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

import androidx.lifecycle.ViewModelProvider;

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle 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.

        MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // 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 はそのスコープに関連付けられます。ViewModel は、スコープに設定されている ViewModelStoreOwner が消えるまでメモリ内にとどまります。これは、次のような状況で発生します。

  • アクティビティの場合は終了時。
  • フラグメントの場合はデタッチ時。
  • Navigation エントリの場合はバックスタックからの削除時。

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

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

ViewModel のライフサイクルをアクティビティの状態の変化とともに図で示します。

通常は、アクティビティ オブジェクトの onCreate() メソッドが最初に呼び出されたときに、ViewModel をリクエストします。onCreate() は、デバイスの画面が回転されたときなど、アクティビティの存続期間全体を通して複数回呼び出されることがあります。ViewModel は、最初に ViewModel をリクエストしてからアクティビティが終了して破棄されるまで存在します。

ベスト プラクティス

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 クラスについて詳しくは、以下のリソースをご覧ください。

ドキュメント

サンプル