Mô-đun Trạng thái đã lưu của ViewModel   Một phần của Android Jetpack.

Như đã đề cập trong phần Lưu trạng thái của giao diện người dùng, đối tượng ViewModel có thể xử lý các thay đổi về cấu hình nên bạn không cần lo lắng về trạng thái khi ở chế độ xoay hoặc các trường hợp khác. Tuy nhiên, nếu cần xử lý tình huống bị buộc tắt do hệ thống gây ra, có thể bạn sẽ muốn dùng API SavedStateHandle để dự phòng.

Trạng thái giao diện người dùng thường được lưu trữ hoặc có thể tham chiếu trong đối tượng ViewModel và không phải là hoạt động, nên việc sử dụng onSaveInstanceState() hoặc rememberSaveable yêu cầu một số mã nguyên mẫu mà mô-đun trạng thái đã lưu có thể xử lý giúp bạn.

Khi sử dụng mô-đun này, các đối tượng ViewModel sẽ nhận được đối tượng SavedStateHandle qua hàm khởi tạo của nó. Đối tượng này là một bản đồ khoá-giá trị cho phép bạn viết và truy xuất đối tượng đến và đi từ trạng thái đã lưu. Các giá trị này vẫn tồn tại sau khi hệ thống loại bỏ quy trình và duy trì thông qua cùng một đối tượng.

Trạng thái đã lưu gắn liền với ngăn xếp tác vụ. Nếu ngăn xếp tác vụ biến mất, trạng thái tác vụ đã lưu cũng sẽ biến mất. Điều này có thể xảy ra khi buộc một ứng dụng dừng, xoá ứng dụng khỏi trình đơn gần đây hoặc khởi động lại thiết bị. Trong những trường hợp như vậy, ngăn xếp tác vụ sẽ biến mất và bạn không thể khôi phục thông tin ở trạng thái đã lưu. Trong các trường hợp đóng trạng thái giao diện người dùng do người dùng gây ra, trạng thái đã lưu sẽ không được khôi phục. Trong các trường hợp do hệ thống gây ra, trạng thái đã lưu sẽ được khôi phục.

Thiết lập

Kể từ Mảnh 1.2.0 hoặc phần phụ thuộc bắc cầu Hoạt động 1.1.0, bạn có thể chấp nhận SavedStateHandle làm đối số hàm khởi tạo cho ViewModel của mình.

Kotlin

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

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

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

    ...
}

Sau đó, bạn có thể truy xuất phiên bản ViewModel mà không cần cấu hình bổ sung. Nhà máy ViewModel mặc định sẽ cung cấp SavedStateHandle phù hợp cho ViewModel của bạn.

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

        ...

    }

    ...
}

Khi cung cấp phiên bản ViewModelProvider.Factory tuỳ chỉnh, bạn có thể cho phép việc sử dụng SavedStateHandle bằng cách mở rộng AbstractSavedStateViewModelFactory.

Làm việc với SavedStateHandle

Lớp SavedStateHandle là một bản đồ khoá-giá trị cho phép bạn ghi và truy xuất dữ liệu đến và đi từ trạng thái đã lưu thông qua phương thức set()get().

Bằng cách dùng SavedStateHandle, giá trị truy vấn sẽ được giữ lại khi ứng dụng bị buộc tắt để đảm bảo người dùng xem được cùng một nhóm dữ liệu đã lọc trước và sau khi tạo lại mà không có hoạt động hoặc mảnh cần lưu, khôi phục thủ công và chuyển tiếp giá trị đó trở lại ViewModel.

SavedStateHandle cũng cung cấp các phương thức khác mà bạn có thể thấy khi tương tác với bản đồ khoá-giá trị:

Ngoài ra, nếu muốn truy xuất các giá trị từ SavedStateHandle, hãy dùng phần tử giữ dữ liệu có thể ghi nhận được. Danh sách các loại được hỗ trợ là:

LiveData

Nếu muốn truy xuất các giá trị từ SavedStateHandle được gói trong một LiveData có thể ghi nhận được, hãy dùng getLiveData(). Khi giá trị của khoá đã được cập nhật, LiveData sẽ nhận được giá trị mới. Thông thường, giá trị này được đặt do tương tác của người dùng, chẳng hạn như nhập truy vấn để lọc danh sách dữ liệu. Sau đó, bạn có thể dùng giá trị đã cập nhật này để chuyển đổi 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

Nếu muốn truy xuất các giá trị từ SavedStateHandle được gói trong một StateFlow có thể ghi nhận được, hãy dùng getStateFlow(). Khi bạn cập nhật giá trị của khoá, StateFlow sẽ nhận được giá trị mới. Thông thường, bạn có thể đặt giá trị do hoạt động tương tác của người dùng, chẳng hạn như nhập truy vấn để lọc danh sách dữ liệu. Sau đó, bạn có thể chuyển đổi giá trị đã cập nhật này bằng các toán tử Luồng (Flow) khác.

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

Hỗ trợ trạng thái của Compose thử nghiệm

Cấu phần phần mềm lifecycle-viewmodel-compose cung cấp API saveable thử nghiệm cho phép SavedStateHandleSaver của Compose tương tác với nhau để bất kỳ State nào mà bạn có thể lưu qua rememberSaveable với một Saver tuỳ chỉnh cũng lưu được bằng 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
        }
    }
}

Các kiểu được hỗ trợ

Dữ liệu có trong SavedStateHandle được lưu và khôi phục dưới dạng Bundle cùng với phần còn lại của savedInstanceState cho hoạt động hoặc mảnh.

Các loại được hỗ trợ trực tiếp

Theo mặc định, bạn có thể gọi set()get() trên SavedStateHandle cho các loại dữ liệu tương tự Bundle như sau:

Hỗ trợ loại/lớp Hỗ trợ mảng
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+)

Nếu lớp không bao gồm một trong các lớp nêu trên, hãy cân nhắc việc tạo lớp theo gói bằng cách thêm chú thích @Parcelize trong Kotlin hoặc triển khai Parcelable trực tiếp.

Lưu các lớp không theo gói

Nếu một lớp không triển khai Parcelable hoặc Serializable và không thể sửa đổi để triển khai một trong các giao diện, bạn không trực tiếp lưu được bản sao của lớp đó vào SavedStateHandle.

Kể từ Lifecycle 2.3.0-alpha03, với SavedStateHandle, bạn có thể lưu mọi đối tượng bằng cách đưa ra logic của riêng mình để lưu và khôi phục đối tượng thành Bundle theo phương thức setSavedStateProvider(). SavedStateRegistry.SavedStateProvider là giao diện xác định một phương thức saveState() trả về Bundle chứa trạng thái bạn muốn lưu. Khi đã sẵn sàng để lưu trạng thái, SavedStateHandle sẽ gọi saveState() để truy xuất Bundle từ SavedStateProvider và lưu Bundle cho khoá đã liên kết.

Hãy xem xét ví dụ về một ứng dụng yêu cầu hình ảnh từ ứng dụng máy ảnh thông qua ý định ACTION_IMAGE_CAPTURE và chuyển tệp tạm thời để xác định vị trí mà máy ảnh nên lưu hình ảnh. TempFileViewModel đóng gói logic để tạo tệp tạm thời đó.

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

Để đảm bảo tệp tạm thời không mất đi nếu quy trình hoạt động bị dừng không mong muốn và khôi phục về sau, TempFileViewModel có thể sử dụng SavedStateHandle để duy trì dữ liệu. Để cho phép TempFileViewModel lưu dữ liệu, hãy triển khai SavedStateProvider và đặt làm trình cung cấp trên SavedStateHandle của 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;
        }
    }
}

Để khôi phục dữ liệu của File khi người dùng quay lại, hãy truy xuất temp_file Bundle từ SavedStateHandle. Đây là cùng một Bundle do saveTempFile() cung cấp có chứa đường dẫn tuyệt đối. Sau đó, đường dẫn tuyệt đối có thể dùng để tạo File mới.

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 trong hoạt động kiểm thử

Để kiểm thử một ViewModel lấy SavedStateHandle làm phần phụ thuộc, hãy tạo thực thể mới cho SavedStateHandle có giá trị kiểm thử theo yêu cầu và truyền nó vào thực thể ViewModel mà bạn đang kiểm thử.

Kotlin

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

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

Tài nguyên khác

Để biết thêm thông tin về mô-đun Trạng thái đã lưu của ViewModel, hãy xem các tài nguyên sau đây.

Lớp học lập trình