ViewModel の保存済み状態のモジュール   Android Jetpack の一部。

UI の状態の保存で説明したように、ViewModel オブジェクトは 構成の変更を処理できるため、ローテーション などの状態を気にする必要はありません。ただし、システムによって開始されたプロセスの終了を処理する必要がある場合は、バックアップとして SavedStateHandle API を使用することをおすすめします。

UI の状態は通常、ViewModel オブジェクトに保存されるか、このオブジェクトで参照されます。そのため Compose で rememberSaveable を使用するには、 保存済み状態モジュールで処理できるボイラープレートが必要です。

このモジュールを使用すると、ViewModel オブジェクトはコンストラクタを介して SavedStateHandle オブジェクトを受け取ります。このオブジェクトは、保存済み状態との間でオブジェクトの書き込みや取得を行えるようにする Key-Value マップです。これらの値は、システムによってプロセスが強制終了された後も保持され、同じオブジェクトを介して引き続き使用できます。

保存済み状態はタスクスタックに関連付けられます。タスクスタックがなくなると、保存済みの状態もなくなります。これは、アプリを強制停止する、[最近] メニューからアプリを削除する、デバイスを再起動するときに発生することがあります。その場合、タスクスタックが表示されなくなり、保存済みの状態の情報を復元できなくなります。ユーザーが開始する UI の状態の破棄シナリオでは、保存済みの状態は復元されません。システムが開始するシナリオでは復元されます。

設定

SavedStateHandle を使用するには、ViewModel のコンストラクタ引数として受け取ります。

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

その後、追加の構成なしでコンポーザブル内で ViewModel のインスタンスを取得できます。デフォルトの ViewModel Factory は、適切な SavedStateHandleViewModel に提供します。

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

カスタム ViewModelProvider.Factory インスタンスを提供する場合、 SavedStateHandle の使用を CreationExtrasviewModelFactory DSL を使用して有効にできます。

SavedStateHandle を使用する

SavedStateHandle クラスは、set() メソッドおよび get() メソッドを介して、保存済み状態との間でデータの書き込みや 取得を行えるようにする Key-Value マップです。

SavedStateHandle を使用すると、クエリ値がプロセスの終了後も保持されるため、再作成の前と後で同じフィルタ処理が施されたデータのセットがユーザーに表示されるようになります。アクティビティやフラグメントを手動で保存、復元し、その値を ViewModel に転送して戻す必要はありません。

SavedStateHandle では、Key-Value マップの操作で役立つ可能性のあるメソッドが他にも用意されています。

  • contains(String key) - 指定されたキーの値が存在するかどうかを確認します。
  • remove(String key) - 指定されたキーの値を削除します。
  • keys() - SavedStateHandle に含まれるすべてのキーを返します。

また、観測可能なデータホルダーを使用して SavedStateHandle から値を取得できます。サポートされている型は次のとおりです。

StateFlow

StateFlow オブザーバブルでラップされた SavedStateHandle から値を取得できます。値を直接ミューテーションする必要があるかどうかに応じて、読み取り専用ストリームとミュータブル ストリームのどちらかを選択できます。

  • getStateFlow(): 状態を読み取るだけでよい場合に使用します。SavedStateHandle の別の場所でキーの値を更新すると、StateFlow は新しい値を受け取ります。読み取り専用ストリームを公開し、Flow オペレータを使用して変換する場合に最適です。
  • getMutableStateFlow(): 読み取りと書き込みの両方のアクセス権が必要な場合に使用します。返された MutableStateFlow.value を更新すると、基盤となる SavedStateHandle が自動的に更新されるため、キーを手動で設定する必要はありません。

ほとんどの場合、これらの値は、データのリストをフィルタリングするためのクエリ入力など、ユーザー操作により更新されます。

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    // Use getMutableStateFlow to read and write the query directly
    private val _query = savedStateHandle.getMutableStateFlow("query", "")
    val query: StateFlow = _query.asStateFlow()

    // Use getStateFlow if you only need a read-only stream to react to changes
    val filteredData: StateFlow<List> =
        query.flatMapLatest {
            repository.getFilteredData(it)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun setQuery(newQuery: String) {
        // Updating the MutableStateFlow automatically updates the SavedStateHandle
        _query.value = newQuery
    }
}

KotlinX シリアル化のサポート

複雑な UI 状態の場合は、KotlinX シリアル化とともに saved プロパティ デリゲートを使用できます。このデリゲートを使用すると、カスタムの @Serializable データクラスを SavedStateHandle に直接保持できます。これにより、プロセスの終了後も ViewModel の状態が保持されるため、Compose UI は再作成時に状態をシームレスに復元できます。

これを使用するには、データクラスに @Serializable アノテーションを付け、ViewModel で saved デリゲートを使用します。

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
// Ensure you have the savedstate-ktx dependency
import androidx.savedstate.serialization.saved
import kotlinx.serialization.Serializable

@Serializable
data class UserFilterState(
    val searchQuery: String,
    val minAge: Int,
    val includeInactive: Boolean
)

class FilterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {

    // The state is automatically serialized to a Bundle on process death,
    // and deserialized upon recreation.
    var filterState by savedStateHandle.saved {
        UserFilterState(searchQuery = "", minAge = 18, includeInactive = false)
    }

    fun updateQuery(newQuery: String) {
        // Mutating the property automatically updates the underlying SavedStateHandle
        filterState = filterState.copy(searchQuery = newQuery)
    }
}

Compose State のサポート

状態が KotlinX シリアル化ではなく Compose の Saver API に依存している場合、lifecycle-viewmodel-compose アーティファクトは saveable デリゲートを提供します。これにより、 SavedStateHandle と Compose's Saver の相互運用が可能になり、State なら、 save via rememberSaveable とカスタム Saver を使って保存できる なら、 SavedStateHandle を使って保存することもできるようになります。

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    var filteredData: List<String> by savedStateHandle.saveable {
        mutableStateOf(emptyList())
    }

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData += query
        }
    }
}

サポートされているタイプ

SavedStateHandle 内に保持されるデータは、アプリの残りの savedInstanceState とともに、Bundle として保存、復元されます。

直接サポートされている型

デフォルトでは、以下に示すように、Bundle と同じデータ型の SavedStateHandleset()get() を呼び出すことができます。

型 / クラスのサポート 配列のサポート
double double[]
int int[]
long long[]
String String[]
byte byte[]
char char[]
CharSequence CharSequence[]
float float[]
Parcelable Parcelable[]
Serializable Serializable[]
short short[]
SparseArray
Binder
Bundle
ArrayList
Size (only in API 21+)
SizeF (only in API 21+)

クラスが上記のリストのいずれも拡張しない場合は、クラスを Parcelable にすることを検討してください。これには、@Parcelize Kotlin アノテーションを追加するか、Parcelable を直接実装します。

非 Parcelable クラスを保存する

クラスが ParcelableSerializable を実装しておらず、これらのインターフェースのいずれかを実装するように変更できない場合、そのクラスのインスタンスを SavedStateHandle に直接保存できません。

Lifecycle 2.3.0-alpha03 以降、SavedStateHandle では、オブジェクトを Bundle として保存 / 復元するための独自ロジックを提供することで、任意のオブジェクトを保存できるようになりました。setSavedStateProvider()SavedStateRegistry.SavedStateProvider は、保存する状態を含む Bundle を返す 単一の saveState() メソッドを定義するインターフェースです。SavedStateHandle は状態を保存できるようになると、saveState() を呼び出して SavedStateProvider から Bundle を取得し、関連するキーの Bundle を保存します。

`ACTION_IMAGE_CAPTURE` インテントを介してカメラアプリから画像をリクエストし、カメラが画像を保存する場所に関する一時ファイルを渡すアプリの例を考えます。ACTION_IMAGE_CAPTURETempFileViewModel は、この一時ファイルを作成するロジックをカプセル化します。

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

TempFileViewModelSavedStateHandle を使用してデータを保持することで、アクティビティのプロセスの強制終了後に復元された際、一時ファイルが消失されないようにできます。TempFileViewModel でデータを保存できるようにするには、 SavedStateProvider を実装し、SavedStateHandleViewModel でプロバイダとして設定します。

private fun File.saveTempFile() = bundleOf("path", absolutePath)

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

ユーザーが戻ったときに File データを復元するには、SavedStateHandle から temp_file Bundle を取得します。これは、絶対パスを含む saveTempFile() により提供される Bundle と同じです。この絶対パスを使用して、新しい File をインスタンス化できます。

private fun File.saveTempFile() = bundleOf("path", absolutePath)

private fun Bundle.restoreTempFile() = if (containsKey("path")) {
    File(getString("path"))
} else {
    null
}

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        val tempFileBundle = savedStateHandle.get<Bundle>("temp_file")
        if (tempFileBundle != null) {
            tempFile = tempFileBundle.restoreTempFile()
        }
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
      return tempFile ?: File.createTempFile("temp", null).also {
          tempFile = it
      }
    }
}

テストで SavedStateHandle を使用する

SavedStateHandle を依存関係として受け取る ViewModel をテストするには、必要なテスト値で SavedStateHandle の新しいインスタンスを作成し、テストする ViewModel インスタンスに渡します。

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

参考情報

ViewModel の保存済み状態モジュールの詳細については、以下のリソースをご覧ください。

Codelab

Views のコンテンツ