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

Các khái niệm và cách triển khai Jetpack Compose

Như đã đề cập trong phần Lưu trạng thái giao diện người dùng, các đối tượng ViewModel có thể xử lý các thay đổi về cấu hình, vì vậy, 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() 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 một đối tượng SavedStateHandle thông 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 một phiên bản ViewModelProvider.Factory tuỳ chỉnh, bạn có thể cho phép 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 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ể dùng giá trị đã cập nhật này để biế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);
    }
}

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.

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, SavedStateHandle cho phép bạn lưu mọi đối tượng bằng cách cung cấp logic của riêng bạn để lưu và khôi phục đối tượng dưới dạng Bundle bằng phương thức setSavedStateProvider(). SavedStateRegistry.SavedStateProvider là một giao diện xác định một phương thức saveState() duy nhất trả về một Bundle chứa trạng thái bạn muốn lưu. Khi SavedStateHandle đã sẵn sàng lưu trạng thái, nó sẽ gọi saveState() để truy xuất Bundle từ SavedStateProvider và lưu Bundle cho khoá được 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, truyề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;
        }
    }
}