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

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

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

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

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

セットアップ

Fragment 1.2.0 およびその推移的な依存関係 Activity 1.1.0 以降、ViewModel のコンストラクタ引数として SavedStateHandle を受け入れることができるようになりました。

Kotlin

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

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        state = savedStateHandle;
    }

    ...
}

その後、追加の構成なしで ViewModel のインスタンスを取得できます。デフォルトの ViewModel ファクトリにより、適切な SavedStateHandleViewModel に提供されます。

Kotlin

class MainFragment : Fragment() {
    val vm: SavedStateViewModel by viewModels()

    ...
}

Java

class MainFragment extends Fragment {
    private SavedStateViewModel vm;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        vm = new ViewModelProvider(this).get(SavedStateViewModel.class);

        ...


    }

    ...
}

カスタム ViewModelProvider.Factory インスタンスを提供する場合、AbstractSavedStateViewModelFactory を拡張することによって SavedStateHandle の使用を有効にできます。

SavedStateHandle を使用する

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

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

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

  • contains(String key) - 指定されたキーの値の有無を確認します。
  • remove(String key) - 指定されたキーの値を削除します。
  • keys() - SavedStateHandle 内に含まれるすべてのキーを返します。

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

LiveData

getLiveData() を使用して LiveData オブザーバブル内にラップされている値を SavedStateHandle から取得します。キーの値が更新されると、LiveData は新しい値を受け取ります。この値はほとんどの場合、データのリストをフィルタリングするためのクエリ入力など、ユーザー操作により設定されます。この更新された値は、LiveData の変換に使用できます。

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: LiveData<List<String>> =
        savedStateHandle.getLiveData<String>("query").switchMap { query ->
        repository.getFilteredData(query)
    }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle savedStateHandle;
    public LiveData<List<String>> filteredData;
    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        this.savedStateHandle = savedStateHandle;
        LiveData<String> queryLiveData = savedStateHandle.getLiveData("query");
        filteredData = Transformations.switchMap(queryLiveData, query -> {
            return repository.getFilteredData(query);
        });
    }

    public void setQuery(String query) {
        savedStateHandle.set("query", query);
    }
}

StateFlow

getStateFlow() を使用して StateFlow オブザーバブル内にラップされている値を SavedStateHandle から取得します。キーの値を更新すると、StateFlow は新しい値を受け取ります。この値は通常、データのリストをフィルタリングするクエリの入力のような、ユーザー操作により設定されます。この更新された値は、他の Flow の演算子を使用して変換できます。

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: StateFlow<List<String>> =
        savedStateHandle.getStateFlow<String>("query")
            .flatMapLatest { query ->
                repository.getFilteredData(query)
            }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

試験運用版 Compose の State のサポート

lifecycle-viewmodel-compose アーティファクトは、試験運用版の saveable SavedStateHandle と Compose の相互運用性を実現する API Saverそのため、State rememberSaveable で保存可能 カスタムの SaverSavedStateHandle でも保存できます。

Kotlin

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+)

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

非 Parcelable クラスを保存する

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

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

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

Kotlin

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

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

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel() {
    }


    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }
}

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

Kotlin

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

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        savedStateHandle.setSavedStateProvider("temp_file",
            new TempFileSavedStateProvider());
    }
    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }
    }
}

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

Kotlin

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

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        Bundle tempFileBundle = savedStateHandle.get("temp_file");
        if (tempFileBundle != null) {
            tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle);
        }
        savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider());
    }

    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }

        @Nullable
        private static File restoreTempFile(Bundle bundle) {
            if (bundle.containsKey("path") {
                return File(bundle.getString("path"));
            }
            return null;
        }
    }
}

テストで SavedStateHandle を使用する

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

Kotlin

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

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

参考情報

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

Codelab