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

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

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

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

セットアップ

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 マップです。また、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 UserViewModelJava(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);
    }
}

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

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

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

サポートされている型

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

参考情報

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

Codelab