Modul Status Tersimpan untuk ViewModel Bagian dari Android Jetpack.

Seperti yang telah disebutkan dalam Menyimpan Status UI, objek ViewModel dapat menangani perubahan konfigurasi, sehingga Anda tidak perlu khawatir tentang status dalam rotasi atau kasus lainnya. Namun, jika Anda perlu menangani penghentian proses yang diinisiasi sistem, Anda dapat menggunakan SavedStateHandle API sebagai cadangan.

Status UI biasanya disimpan atau dirujuk dalam objek ViewModel dan bukan aktivitas, sehingga penggunaan onSaveInstanceState() atau rememberSaveable memerlukan beberapa boilerplate yang dapat ditangani oleh modul status tersimpan untuk Anda.

Saat menggunakan modul ini, objek ViewModel akan menerima objek SavedStateHandle melalui konstruktornya. Objek ini adalah peta nilai kunci (key value) yang memungkinkan Anda menulis dan mengambil objek ke dan dari status tersimpan. Nilai ini dipertahankan setelah proses dihapus oleh sistem dan tetap tersedia melalui objek yang sama.

Status tersimpan terikat dengan stack tugas Anda. Jika stack tugas Anda hilang, status tersimpan Anda juga akan hilang. Hal ini dapat terjadi saat Anda memaksa aplikasi berhenti, menghapus aplikasi dari menu terbaru, atau memulai ulang perangkat. Dalam kasus semacam ini, stack tugas akan hilang dan Anda tidak dapat memulihkan informasi dalam status tersimpan. Di skenario Penutupan status UI yang diinisialisasi pengguna, status tersimpan tidak dipulihkan. Di skenario yang dimulai oleh sistem, status tersimpan dipulihkan.

Penyiapan

Mulai dari Fragment 1.2.0 atau dependensi transitifnya Activity 1.1.0, Anda dapat menerima SavedStateHandle sebagai argumen konstruktor untuk ViewModel.

Kotlin

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

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

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

    ...
}

Selanjutnya, Anda dapat mengambil instance ViewModel tanpa konfigurasi tambahan apa pun. Factory ViewModel default menyediakan SavedStateHandle yang sesuai untuk ViewModel Anda.

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

        ...


    }

    ...
}

Saat memberikan instance ViewModelProvider.Factory kustom, Anda dapat mengaktifkan penggunaan SavedStateHandle dengan memperluas AbstractSavedStateViewModelFactory.

Menangani SavedStateHandle

Class SavedStateHandle adalah peta nilai kunci yang memungkinkan Anda menulis dan mengambil data ke dan dari status tersimpan melalui metode set() dan get().

Dengan menggunakan SavedStateHandle, nilai kueri akan dipertahankan di seluruh penghentian proses, sehingga memastikan pengguna melihat kumpulan data yang difilter yang sama sebelum dan setelah pembuatan ulang tanpa aktivitas atau fragmen perlu secara manual menyimpan, memulihkan, dan meneruskan nilai tersebut kembali ke ViewModel.

SavedStateHandle juga memiliki metode lain yang dapat Anda gunakan saat berinteraksi dengan peta nilai kunci:

Selain itu, Anda dapat mengambil nilai dari SavedStateHandle menggunakan holder data yang dapat diamati. Daftar jenis yang didukung adalah:

LiveData

Ambil nilai dari SavedStateHandle yang digabungkan dalam observable LiveData menggunakan getLiveData(). Saat nilai kunci diperbarui, LiveData akan menerima nilai baru. Biasanya, nilai ditetapkan karena interaksi pengguna, seperti memasukkan kueri untuk memfilter daftar data. Nilai yang diperbarui ini selanjutnya dapat digunakan untuk mengubah 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

Ambil nilai dari SavedStateHandle yang digabungkan dalam StateFlow yang dapat diamati menggunakan getStateFlow(). Saat Anda memperbarui nilai kunci, StateFlow akan menerima nilai baru. Biasanya, Anda dapat menetapkan nilai karena interaksi pengguna, seperti memasukkan kueri untuk memfilter daftar data. Anda kemudian dapat mengubah nilai yang diperbarui ini menggunakan operator Flow lainnya.

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

Dukungan Status Compose Eksperimental

Artefak lifecycle-viewmodel-compose menyediakan alat eksperimental saveable API yang memungkinkan interoperabilitas antara SavedStateHandle dan Compose Saver sehingga setiap State yang Anda dapat disimpan melalui rememberSaveable dengan Saver kustom juga dapat disimpan dengan SavedStateHandle.

Kotlin

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

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

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

Jenis yang didukung

Data yang dipertahankan dalam SavedStateHandle disimpan dan dipulihkan sebagai Bundle, beserta savedInstanceState lainnya untuk aktivitas atau fragmen tersebut.

Jenis yang didukung secara langsung

Secara default, Anda dapat memanggil set() dan get() di SavedStateHandle untuk jenis data yang sama dengan Bundle, seperti yang ditunjukkan di bawah ini:

Dukungan Jenis/Class Dukungan array
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+)

Jika class tidak memperluas salah satu yang ada dalam daftar di atas, pertimbangkan untuk membagi-bagi class dengan menambahkan anotasi Kotlin @Parcelize atau mengimplementasikan Parcelable secara langsung.

Menyimpan class yang tidak dapat dibagi-bagi

Jika class tidak mengimplementasi Parcelable atau Serializable dan tidak dapat dimodifikasi untuk mengimplementasikan salah satu antarmuka tersebut, maka Anda tidak dapat menyimpan instance class tersebut secara langsung ke dalam SavedStateHandle.

Mulai dari Lifecycle 2.3.0-alpha03, SavedStateHandle memungkinkan Anda menyimpan objek apa pun dengan menyediakan logika sendiri untuk menyimpan dan memulihkan objek sebagai Bundle menggunakan metode setSavedStateProvider(). SavedStateRegistry.SavedStateProvider adalah antarmuka yang menentukan satu metode saveState() yang menampilkan Bundle berisi status yang ingin Anda simpan. Saat SavedStateHandle siap menyimpan statusnya, saveState() akan dipanggil untuk mengambil Bundle dari SavedStateProvider dan menyimpan Bundle untuk kunci terkait.

Mari kita gunakan contoh sebuah aplikasi yang meminta gambar dari aplikasi kamera melalui intent ACTION_IMAGE_CAPTURE, yang meneruskan file sementara tempat kamera akan menyimpan gambar. TempFileViewModel merangkum logika untuk membuat file sementara tersebut.

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

Untuk memastikan file sementara tidak hilang jika proses aktivitas dihentikan dan kemudian dipulihkan, TempFileViewModel dapat menggunakan SavedStateHandle untuk mempertahankan datanya. Untuk mengizinkan TempFileViewModel menyimpan datanya, implementasikan SavedStateProvider dan tetapkan sebagai penyedia pada SavedStateHandle untuk ViewModel:

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

Untuk memulihkan data File saat pengguna kembali, ambil temp_file Bundle dari SavedStateHandle. Ini adalah Bundle yang sama yang disediakan oleh saveTempFile() berisi jalur absolut. Jalur absolut tersebut kemudian dapat digunakan untuk membuat instance File baru.

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 dalam pengujian

Untuk menguji ViewModel yang menggunakan SavedStateHandle sebagai dependensi, buat instance SavedStateHandle baru dengan nilai pengujian yang diperlukan dan teruskan ke instance ViewModel yang sedang Anda uji.

Kotlin

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

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

Referensi lainnya

Guna mengetahui informasi modul Status Tersimpan untuk ViewModel lebih lanjut, baca referensi berikut.

Codelab