Lưu trạng thái giao diện người dùng

Hướng dẫn này thảo luận về kỳ vọng của người dùng đối với trạng thái giao diện người dùng (UI) và các tuỳ chọn để duy trì trạng thái.

Việc lưu và nhanh chóng khôi phục trạng thái giao diện người dùng của một hoạt động sau khi hệ thống huỷ bỏ các hoạt động hoặc ứng dụng là điều cần thiết để mang lại trải nghiệm tốt cho người dùng. Người dùng mong muốn trạng thái giao diện người dùng vẫn giữ nguyên, nhưng hệ thống có thể huỷ bỏ hoạt động và trạng thái đã lưu trữ.

Để thu hẹp khoảng cách giữa kỳ vọng của người dùng và hành vi của hệ thống, hãy sử dụng kết hợp các phương thức sau:

  • Đối tượng ViewModel.
  • Trạng thái thực thể đã lưu trong các bối cảnh sau:
  • Bộ nhớ cục bộ để duy trì trạng thái giao diện người dùng trong quá trình chuyển đổi ứng dụng và hoạt động.

Giải pháp tối ưu phụ thuộc vào độ phức tạp của dữ liệu giao diện người dùng, các trường hợp sử dụng của ứng dụng, cũng như việc cân bằng giữa tốc độ truy cập dữ liệu và mức sử dụng bộ nhớ.

Đảm bảo ứng dụng đáp ứng kỳ vọng của người dùng và cung cấp giao diện phản hồi nhanh. Tránh chậm trễ khi tải dữ liệu vào UI, đặc biệt là sau khi cấu hình thay đổi theo các cách thường gặp, chẳng hạn như xoay.

Kỳ vọng của người dùng và hành vi của hệ thống

Tuỳ thuộc vào hành động mà người dùng thực hiện, họ sẽ mong muốn hệ thống xoá hoặc duy trì trạng thái hoạt động. Trong một số trường hợp, hệ thống tự động làm những việc người dùng mong đợi. Trong các trường hợp khác, hệ thống lại làm ngược lại những gì người dùng mong đợi.

Thao tác đóng trạng thái giao diện người dùng do người dùng khởi tạo

Người dùng muốn khi họ bắt đầu một hoạt động, trạng thái tạm thời của giao diện người dùng trong hoạt động đó sẽ vẫn giữ nguyên cho đến khi người dùng đóng hoàn toàn hoạt động đó. Người dùng có thể đóng hoàn toàn một hoạt động bằng các cách sau:

  • Vuốt hoạt động ra khỏi màn hình Tổng quan (Gần đây)
  • Tắt hoặc buộc thoát khỏi ứng dụng từ màn hình Cài đặt.
  • Khởi động lại thiết bị.
  • Thực hiện một số thao tác "hoàn tất" (được Activity.finish() hỗ trợ).

Giả định của người dùng trong những trường hợp đóng hoàn toàn hoạt động này là họ đã điều hướng vĩnh viễn khỏi hoạt động và nếu mở lại hoạt động, họ muốn hoạt động sẽ bắt đầu từ một trạng thái mới. Hành vi cơ bản của hệ thống trong các tình huống đóng hoạt động này khớp với kỳ vọng của người dùng – thực thể của hoạt động sẽ bị huỷ bỏ và xoá khỏi bộ nhớ, đồng thời, mọi trạng thái được lưu trữ trong đó và mọi bản ghi trạng thái thực thể đã lưu có liên kết với hoạt động đó cũng sẽ bị xoá.

Có một số ngoại lệ đối với quy tắc về thao tác đóng hoàn toàn này – ví dụ: Người dùng có thể muốn trình duyệt đưa họ đến đúng trang web mà họ đang xem trước khi thoát khỏi trình duyệt bằng cách sử dụng nút quay lại.

Thao tác đóng trạng thái giao diện người dùng do hệ thống khởi tạo

Người dùng muốn trạng thái giao diện người dùng của một hoạt động vẫn giữ nguyên trong suốt quá trình thay đổi cấu hình, chẳng hạn như khi thực hiện thao tác xoay hoặc khi chuyển sang chế độ nhiều cửa sổ. Tuy nhiên, theo mặc định, hệ thống sẽ huỷ hoạt động khi xảy ra thay đổi về cấu hình như vậy, xoá sạch mọi trạng thái giao diện người dùng đã lưu trong thực thể của hoạt động đó. Để tìm hiểu thêm về cấu hình thiết bị, hãy xem trang tham chiếu Cấu hình. Lưu ý: Mặc dù không nên, nhưng bạn vẫn có thể ghi đè hành vi mặc định cho những thay đổi về cấu hình. Hãy xem bài viết Tự xử lý sự thay đổi về cấu hình để biết thêm chi tiết.

Người dùng cũng mong muốn trạng thái giao diện người dùng hoạt động của bạn vẫn giữ nguyên nếu họ tạm thời chuyển sang một ứng dụng khác và sau đó quay lại ứng dụng của bạn. Ví dụ: Người dùng thực hiện việc tìm kiếm trong hoạt động tìm kiếm của bạn và sau đó nhấn nút màn hình chính hoặc trả lời một cuộc điện thoại – khi quay lại hoạt động tìm kiếm, họ muốn thấy từ khoá và kết quả tìm kiếm vẫn ở đó giống như trước.

Trong trường hợp này, ứng dụng của bạn được đặt ở chế độ nền, hệ thống sẽ cố gắng hết sức để giữ cho quá trình xử lý ứng dụng luôn ở trong bộ nhớ. Tuy nhiên, hệ thống có thể huỷ bỏ quá trình xử lý ứng dụng trong khi người dùng thoát ra ngoài để tương tác với ứng dụng khác. Trong trường hợp như vậy, hệ thống sẽ huỷ bỏ thực thể hoạt động và bất kỳ trạng thái nào được lưu trữ trong thực thể đó. Khi người dùng chạy lại ứng dụng, hoạt động sẽ ở trạng thái mới không giống như mong đợi. Để tìm hiểu thêm về trường hợp bị buộc tắt, hãy xem bài viết Các quá trình xử lý và vòng đời ứng dụng.

Các tuỳ chọn để duy trì trạng thái giao diện người dùng

Khi kỳ vọng của người dùng về trạng thái giao diện người dùng không khớp với hành vi mặc định của hệ thống, bạn phải lưu và khôi phục trạng thái giao diện người dùng để đảm bảo rằng quá trình huỷ bỏ do hệ thống bắt đầu là minh bạch đối với người dùng.

Mỗi tuỳ chọn duy trì trạng thái giao diện người dùng sẽ khác nhau dựa theo các phương diện sau đây có tác động đến trải nghiệm người dùng:

ViewModel Trạng thái của thực thể đã lưu Bộ nhớ liên tục
Vị trí lưu trữ trong bộ nhớ trong bộ nhớ trên đĩa hoặc mạng
Còn lại sau khi thay đổi cấu hình
Còn lại sau quá trình dừng hoạt động do hệ thống gây ra Không
Còn lại sau khi người dùng hoàn tất thao tác đóng hoạt động/onFinish() Không Không
Hạn mức dữ liệu các đối tượng phức tạp vẫn ổn, nhưng không gian bị giới hạn bởi bộ nhớ hiện có chỉ dành cho các loại nguyên hàm và các đối tượng nhỏ, đơn giản như Chuỗi chỉ bị giới hạn bởi dung lượng ổ đĩa hoặc chi phí/thời gian truy xuất từ tài nguyên mạng
Thời gian đọc/ghi nhanh (chỉ truy cập bộ nhớ) chậm (đòi hỏi chuyển đổi tuần tự/huỷ chuyển đổi tuần tự) chậm (đòi hỏi truy cập vào ổ đĩa hoặc giao dịch trên mạng)

Sử dụng ViewModel để xử lý các thay đổi về cấu hình

ViewModel là lớp lý tưởng để lưu trữ và quản lý dữ liệu liên quan đến giao diện người dùng trong khi người dùng đang chủ động sử dụng ứng dụng. Với ViewModel, bạn có thể truy cập nhanh vào dữ liệu giao diện người dùng mà không cần tìm nạp lại dữ liệu từ mạng hoặc ổ đĩa khi xoay, đổi kích thước cửa sổ và thực hiện các thay đổi phổ biến khác về cấu hình. Để tìm hiểu cách triển khai ViewModel, hãy xem bài viết Hướng dẫn về ViewModel.

ViewModel giữ lại dữ liệu trong bộ nhớ, tức là việc truy xuất dữ liệu từ đây sẽ rẻ hơn so với từ ổ đĩa hoặc mạng. ViewModel liên kết với một hoạt động (hoặc chủ sở hữu khác của vòng đời) – lớp này ở trong bộ nhớ trong suốt quá trình thay đổi cấu hình và hệ thống sẽ tự động liên kết ViewModel với thực thể hoạt động mới là kết quả của quá trình thay đổi cấu hình.

Hệ thống sẽ tự động huỷ bỏ ViewModel khi người dùng của bạn rời khỏi hoạt động hoặc mảnh hoặc khi bạn gọi finish(), có nghĩa là, trong các trường hợp này, trạng thái sẽ bị xoá như người dùng muốn.

Không giống như trạng thái thực thể đã lưu, ViewModel sẽ bị huỷ bỏ trong trường hợp bị hệ thống buộc tắt. Để tải lại dữ liệu sau khi hệ thống buộc tắt trong ViewModel, hãy sử dụng SavedStateHandle API. Ngoài ra, nếu dữ liệu liên quan đến giao diện người dùng và không cần được giữ trong ViewModel, hãy sử dụng onSaveInstanceState() trong hệ thống View hoặc rememberSaveable trong Jetpack Compose. Nếu dữ liệu là dữ liệu ứng dụng, tốt hơn là bạn nên lưu giữ dữ liệu đó vào ổ đĩa.

Nếu đã có sẵn giải pháp trong bộ nhớ để lưu trữ trạng thái giao diện người dùng sau các thay đổi về cấu hình, thì bạn có thể không cần phải sử dụng ViewModel.

Sử dụng trạng thái thực thể đã lưu làm phương án dự phòng để xử lý các trường hợp bị hệ thống buộc tắt

Lệnh gọi lại onSaveInstanceState() trong hệ thống View, rememberSaveable trong Jetpack Compose và SavedStateHandle trong ViewModel lưu trữ dữ liệu cần để tải lại trạng thái của một trình điều khiển giao diện người dùng, chẳng hạn như một hoạt động hoặc mảnh, nếu hệ thống huỷ bỏ rồi tạo lại trình điều khiển đó. Để tìm hiểu cách triển khai trạng thái thực thể đã lưu bằng onSaveInstanceState, hãy xem mục Lưu và khôi phục trạng thái hoạt động trong phần Hướng dẫn về Vòng đời hoạt động.

Gói trạng thái thực thể đã lưu vẫn duy trì qua cả các lần thay đổi cấu hình và bị buộc tắt nhưng bị giới hạn về bộ nhớ và tốc độ, vì có nhiều API chuyển đổi tuần tự dữ liệu. Quá trình chuyển đổi tuần tự có thể tốn nhiều bộ nhớ nếu các đối tượng chuyển đổi tuần tự là rất phức tạp. Vì quá trình này xảy ra trên luồng chính khi cấu hình đang có sự thay đổi, nên việc chuyển đổi tuần tự chạy trong thời gian dài có thể gây ra tình trạng sụt khung hình và gián đoạn hình ảnh.

Không sử dụng trạng thái thực thể đã lưu để lưu nhiều dữ liệu, chẳng hạn như bitmap hoặc cấu trúc dữ liệu phức tạp đòi hỏi quá trình chuyển đổi tuần tự hoặc huỷ chuyển đổi tuần tự chạy trong thời gian dài. Thay vào đó, chỉ lưu trữ các loại nguyên hàm và đối tượng nhỏ, đơn giản như String. Do vậy, hãy sử dụng trạng thái thực thể đã lưu để lưu trữ lượng dữ liệu tối thiểu cần thiết, chẳng hạn như mã nhận dạng, để tạo lại dữ liệu cần thiết nhằm khôi phục giao diện người dùng về trạng thái trước đó nếu các cơ chế cố định khác không hoạt động. Hầu hết các ứng dụng nên triển khai thao tác này để xử lý trường hợp bị buộc tắt do hệ thống gây ra.

Tuỳ thuộc vào các trường hợp sử dụng của ứng dụng, có thể bạn không cần dùng trạng thái thực thể đã lưu. Ví dụ: Một trình duyệt có thể đưa người dùng quay lại đúng trang web mà họ đang xem trước khi thoát khỏi trình duyệt. Nếu hoạt động của bạn hành xử theo cách này, thì có thể bỏ qua việc sử dụng trạng thái thực thể đã lưu và thay vào đó, duy trì mọi thứ một cách cục bộ.

Ngoài ra, khi bạn mở một hoạt động từ một ý định, hệ thống sẽ phân phối gói bổ sung tới hoạt động đó cả khi cấu hình thay đổi và khi hệ thống khôi phục hoạt động. Nếu một phần dữ liệu trạng thái giao diện người dùng, chẳng hạn như cụm từ tìm kiếm, được truyền vào dưới dạng một ý định bổ sung khi hoạt động được khởi chạy, thì bạn có thể sử dụng gói bổ sung thay vì gói trạng thái thực thể đã lưu. Để tìm hiểu thêm về ý định bổ sung, hãy xem bài viết Ý định và bộ lọc ý định.

Trong cả hai trường hợp này, bạn vẫn nên sử dụng ViewModel để tránh lãng phí chu kỳ tải lại dữ liệu từ cơ sở dữ liệu trong quá trình thay đổi cấu hình.

Trong trường hợp dữ liệu giao diện người dùng cần duy trì là đơn giản và gọn nhẹ, bạn có thể chỉ cần sử dụng các API trạng thái thực thể đã lưu để duy trì dữ liệu trạng thái của mình.

Chuyển sang trạng thái đã lưu bằng SavedStateRegistry

Bắt đầu với Mảnh 1.1.0 hoặc phần phụ thuộc chuyển đổi Hoạt động 1.0.0, trình điều khiển giao diện người dùng, chẳng hạn như Activity hoặc Fragment, triển khai SavedStateRegistryOwner và cung cấp SavedStateRegistry liên kết với trình điều khiển đó. SavedStateRegistry cho phép các thành phần chuyển sang trạng thái đã lưu của trình điều khiển giao diện người dùng để tiêu thụ hoặc đóng góp vào trạng thái đó. Ví dụ: Mô-đun trạng thái đã lưu cho ViewModel sử dụng SavedStateRegistry để tạo SavedStateHandle và cung cấp lớp đó cho các đối tượng ViewModel. Bạn có thể truy xuất SavedStateRegistry từ bên trong trình điều khiển giao diện người dùng bằng cách gọi getSavedStateRegistry().

Các thành phần đóng góp vào trạng thái đã lưu phải triển khai SavedStateRegistry.SavedStateProvider, nhằm xác định một phương thức duy nhất có tên là saveState(). Phương thức saveState() cho phép thành phần của bạn trả về một Bundle chứa bất kỳ trạng thái nào lưu trên thành phần đó. SavedStateRegistry gọi phương thức này trong giai đoạn lưu trạng thái trong vòng đời của trình điều khiển giao diện người dùng.

Kotlin

class SearchManager : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val QUERY = "query"
    }

    private val query: String? = null

    ...

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

Để đăng ký SavedStateProvider, hãy gọi registerSavedStateProvider() trên SavedStateRegistry, truyền khoá để liên kết với dữ liệu của nhà cung cấp cũng như với nhà cung cấp. Bạn có thể truy xuất dữ liệu đã lưu trước đây dành cho nhà cung cấp từ trạng thái đã lưu bằng cách gọi consumeRestoredStateForKey() trên SavedStateRegistry, truyền khoá liên kết với dữ liệu của nhà cung cấp.

Trong Activity hoặc Fragment, bạn có thể đăng ký SavedStateProvider trong onCreate() sau khi gọi super.onCreate(). Ngoài ra, bạn có thể đặt LifecycleObserver trên SavedStateRegistryOwner, nhằm triển khai LifecycleOwner và đăng ký SavedStateProvider khi sự kiện ON_CREATE xảy ra. Bằng cách sử dụng LifecycleObserver, bạn có thể tách thao tác đăng ký và truy xuất của trạng thái đã lưu trước đó từ chính SavedStateRegistryOwner.

Kotlin

class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val PROVIDER = "search_manager"
        private const val QUERY = "query"
    }

    private val query: String? = null

    init {
        // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
        registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_CREATE) {
                val registry = registryOwner.savedStateRegistry

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this)

                // Get the previously saved state and restore it
                val state = registry.consumeRestoredStateForKey(PROVIDER)

                // Apply the previously saved state
                query = state?.getString(QUERY)
            }
        }
    }

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }

    ...
}

class SearchFragment : Fragment() {
    private var searchManager = SearchManager(this)
    ...
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this);

                // Get the previously saved state and restore it
                Bundle state = registry.consumeRestoredStateForKey(PROVIDER);

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

Sử dụng tính năng cố định cục bộ để xử lý trường hợp bị buộc tắt đối với dữ liệu lớn hoặc phức tạp

Bộ nhớ cục bộ ổn định, chẳng hạn như cơ sở dữ liệu hoặc các lựa chọn ưu tiên chung, sẽ vẫn còn cho đến khi ứng dụng của bạn được cài đặt trên thiết bị của người dùng (trừ phi người dùng xoá dữ liệu ứng dụng). Mặc dù bộ nhớ cục bộ như vậy vẫn còn sau khi hệ thống buộc tắt ứng dụng và hoạt động, nhưng việc truy xuất có thể tốn kém vì hệ thống sẽ phải đọc dữ liệu từ bộ nhớ cục bộ trong bộ nhớ. Thông thường, bộ nhớ cục bộ ổn định này có thể đã là một phần của cấu trúc ứng dụng để lưu trữ tất cả dữ liệu mà bạn không muốn mất đi nếu mở và đóng hoạt động.

Cả ViewModel và trạng thái thực thể đã lưu đều không phải là giải pháp lưu trữ dài hạn và do đó, chúng không thể là giải pháp thay thế cho bộ nhớ cục bộ, chẳng hạn như cơ sở dữ liệu. Thay vào đó, bạn chỉ nên sử dụng các cơ chế này để lưu trữ ngắn hạn trạng thái giao diện người dùng tạm thời và sử dụng bộ nhớ ổn định cho dữ liệu ứng dụng khác. Xem bài viết Hướng dẫn về cấu trúc ứng dụng để biết thêm thông tin chi tiết về cách tận dụng bộ nhớ cục bộ để duy trì lâu dài dữ liệu mô hình ứng dụng (ví dụ: qua các lần khởi động lại thiết bị).

Quản lý trạng thái giao diện người dùng: chia để trị

Bạn có thể lưu và khôi phục hiệu quả trạng thái giao diện người dùng bằng cách phân chia công việc cho nhiều loại cơ chế cố định. Trong hầu hết các trường hợp, mỗi cơ chế này nên lưu trữ một loại dữ liệu khác nhau được dùng trong hoạt động đó, dựa trên sự đánh đổi về độ phức tạp của dữ liệu, tốc độ truy cập và thời gian tồn tại:

  • Lưu trữ cố định cục bộ: Lưu trữ tất cả dữ liệu ứng dụng mà bạn không muốn mất đi nếu mở và đóng hoạt động.
    • Ví dụ: Một tập hợp các đối tượng bài hát, có thể bao gồm các tệp âm thanh và siêu dữ liệu.
  • ViewModel: Lưu trữ trong bộ nhớ tất cả dữ liệu cần để hiển thị giao diện người dùng liên kết, trạng thái giao diện người dùng màn hình.
    • Ví dụ: Các đối tượng bài hát của hoạt động tìm kiếm gần đây nhất và cụm từ tìm kiếm gần đây nhất.
  • Trạng thái thực thể đã lưu: Lưu trữ một lượng dữ liệu nhỏ cần thiết để tải lại trạng thái giao diện người dùng nếu hệ thống dừng rồi tạo lại giao diện người dùng. Thay vì lưu trữ các đối tượng phức tạp ở đây, hãy duy trì chúng trong bộ nhớ cục bộ và lưu trữ một mã nhận dạng duy nhất cho những đối tượng này trong API trạng thái thực thể đã lưu.
    • Ví dụ: Lưu trữ cụm từ tìm kiếm gần đây nhất.

Ví dụ: Hãy cân nhắc một hoạt động cho phép bạn tìm kiếm trong thư viện bài hát. Dưới đây là cách xử lý các sự kiện khác nhau:

Khi người dùng thêm một bài hát, ViewModel hãy ngay lập tức uỷ quyền duy trì cục bộ dữ liệu này. Nếu bài hát mới thêm này cần hiển thị trong giao diện người dùng, thì bạn cũng nên cập nhật dữ liệu trong đối tượng ViewModel để phản ánh việc thêm bài hát. Hãy nhớ thực hiện tất cả thao tác chèn cơ sở dữ liệu vào luồng chính.

Khi người dùng tìm kiếm một bài hát, dù dữ liệu bài hát bạn tải từ cơ sở dữ liệu phức tạp đến đâu, thì dữ liệu đó phải được lưu trữ ngay lập tức trong đối tượng ViewModel như một phần của trạng thái giao diện người dùng màn hình.

Khi hoạt động chuyển sang trạng thái nền và hệ thống gọi các API trạng thái thực thể đã lưu, cụm từ tìm kiếm phải được lưu trữ trong trạng thái phiên bản đã lưu, trong trường hợp quy trình này được tạo lại. Do vẫn cần thông tin để tải dữ liệu ứng dụng đã tồn tại trong quy trình này nên hãy lưu trữ cụm từ tìm kiếm trong SavedStateHandle ViewModel. Đây là tất cả thông tin bạn cần để tải dữ liệu và đưa giao diện người dùng trở về trạng thái hiện tại.

Khôi phục các trạng thái phức tạp: tập hợp các chi tiết

Sau đây là 2 trường hợp tạo lại hoạt động khi người dùng quay lại hoạt động:

  • Hoạt động được tạo lại sau khi bị hệ thống dừng. Hệ thống sẽ lưu cụm từ tìm kiếm này trong một gói trạng thái thực thể đã lưu và giao diện người dùng phải truyền truy vấn đó đến ViewModel nếu không sử dụng SavedStateHandle. ViewModel thấy rằng không có kết quả tìm kiếm nào được lưu vào bộ nhớ đệm và uỷ quyền tải các kết quả tìm kiếm bằng cụm từ tìm kiếm nhất định.
  • Hoạt động được tạo sau khi thay đổi cấu hình. Do thực thể ViewModel chưa bị huỷ bỏ, nên ViewModel có tất cả thông tin trong bộ nhớ đệm và không cần truy vấn lại cơ sở dữ liệu.

Tài nguyên khác

Để tìm hiểu thêm về cách lưu các trạng thái giao diện người dùng, xem các tài nguyên sau.

Blog