Thao tác lưu trạng thái giao diện người dùng

Việc kịp thời duy trì và khôi phục trạng thái giao diện người dùng của một hoạt động sau khi hoàn tất quá trình huỷ bỏ hoạt động hoặc ứng dụng mà hệ thống bắt đầu là một phần quan trọng trong trải nghiệm người dùng. Trong những trường hợp này, người dùng mong muốn trạng thái giao diện người dùng vẫn giữ nguyên, trong khi đó, hệ thống lại huỷ bỏ hoạt động và bất kỳ trạng thái nào được lưu trữ trong đó.

Để làm cầu nối 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 đối tượng ViewModel, phương thức onSaveInstanceState() và/hoặc bộ nhớ cục bộ để duy trì trạng thái giao diện người dùng sau khi hoàn tất các quá trình chuyển đổi thực thể của hoạt động và ứng dụng này. Việc quyết định cách kết hợp các tùy chọn này phụ thuộc vào độ phức tạp của dữ liệu giao diện người dùng, trường hợp sử dụng của ứng dụng và việc cân nhắc giữa tốc độ truy xuất so với mức sử dụng bộ nhớ.

Dù bạn lựa chọn phương pháp tiếp cận nào, bạn cũng phải đảm bảo ứng dụng của mình đáp ứng sự kỳ vọng của người dùng về trạng thái giao diện người dùng và cung cấp một giao diện người dùng mượt mà, sống động (tránh thời gian trễ khi tải dữ liệu vào giao diện người dùng, đặc biệt là sau các lần thay đổi cấu hình liên tục, chẳng hạn như thao tác xoay). Trong hầu hết các trường hợp, bạn nên sử dụng cả ViewModel và onSaveInstanceState().

Trang này thảo luận về các kỳ vọng của người dùng về trạng thái giao diện người dùng, các tuỳ chọn có sẵn để duy trì trạng thái, những sự đánh đổi và hạn mức của mỗi trạng thái.

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

Tuỳ thuộc vào hành động mà người dùng thực hiện, người dùng mong muốn hệ thống sẽ 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 bắt đầu

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 của hoạt động đó sẽ 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ách:

  • nhấn vào nút quay lại
  • vuốt hoạt động ra khỏi màn hình Tổng quan (Gần đây)
  • di chuyển hoạt động lên
  • tắt ứng dụng trên màn hình Cài đặt
  • hoàn thành một số thao tác "hoàn tất" (do 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 họ 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ỷ 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 của thực thể đã lưu có liên kết với hoạt động đó cũng sẽ bị xoá bỏ.

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 bắt đầu

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. Xem phần Tự xử lý 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 họ 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 đang thoát ra ngoài và tương tác với các ứ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ẽ bất ngờ ở trạng thái mới. Để tìm hiểu thêm về trường hợp dừng hoạt động, hãy xem phần 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 theo các phương diện ảnh hưởng đến trải nghiệm người dùng sau đây:

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ớ chuyển đổi tuần tự ra đĩa 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ỉ có quyền truy cập vào bộ nhớ) chậm (yêu cầu quá trình chuyển đổi tuần tự/huỷ chuyển đổi tuần tự và quyền truy cập vào ổ đĩa) chậm (yêu cầu quyền truy cập vào ổ đĩa hoặc giao dịch trên mạng)

Cách 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. Lớp này cho phép truy cập nhanh vào dữ liệu giao diện người dùng và giúp bạn tránh tìm nạp lại dữ liệu từ mạng hoặc ổ đĩa sau khi hoàn tất thao tác xoay, đổi kích thước cửa sổ và các thay đổi về cấu hình thường gặp khác. Để tìm hiểu cách triển khai ViewModel, hãy xem phần hướng dẫn ViewModel.

ViewModel giữ lại dữ liệu trong bộ nhớ, tức là truy xuất dữ liệu từ đây sẽ rẻ hơn so với dữ liệu từ ổ đĩa hoặc mạng. ViewModel được liên kết với một hoạt động (hoặc chủ sở hữu vòng đời khác) – 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ỷ ViewModels 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, ViewModels sẽ bị huỷ bỏ trong trường hợp dừng hoạt động do hệ thống gây ra. Đây là lý do bạn nên sử dụng kết hợp các đối tượng ViewModel với onSaveInstanceState() (hoặc một số tính năng cố định ổ đĩa khác), lưu trữ giá trị nhận dạng trong savedInstanceState để giúp các mô hình hiển thị tải lại dữ liệu sau khi hệ thống dừng hoạt động.

Nếu bạn đã 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 khi hoàn tất thay đổi về cấu hình, thì bạn có thể không cần phải sử dụng ViewModel.

Cách sử dụng onSaveInstanceState() làm phương thức dự phòng để xử lý các trường hợp dừng hoạt động do hệ thống gây ra

Hàm gọi lại onSaveInstanceState() lưu trữ dữ liệu cần thiết để 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ột mảnh, nếu hệ thống huỷ bỏ và sau đó 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, 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òng đời hoạt động.

Các 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à trường hợp dừng hoạt động nhưng bị giới hạn bởi bộ nhớ và tốc độ, vì onSavedInstanceState() chuyển đổi tuần tự dữ liệu ra đĩa. Quá trình chuyển đổi tuần tự có thể tốn nhiều bộ nhớ nếu các đối tượng được chuyển đổi tuần tự phức tạp. Vì quá trình này xảy ra trên luồng chính trong khi thay đổi cấu hình, nên quá trình 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 phương thức lưu trữ onSavedInstanceState() để lưu trữ một lượng dữ liệu lớn, chẳng hạn như bitmap hoặc các cấu trúc dữ liệu phức tạp yêu cầu quá trình chuyển đổi tuần tự hoặc quá trình huỷ chuyển đổi tuần tự dài. Thay vào đó, hãy chỉ lưu trữ các loại nguyên hàm và các đối tượng nhỏ, đơn giản như String. Theo đó, hãy sử dụng onSaveInstanceState() để lưu trữ một 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 onSaveInstanceState() để xử lý trường hợp dừng hoạt động 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, bạn có thể không cần sử dụng onSaveInstanceState(). 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 xử lý theo cách này, thì bạn có thể chọn trước việc sử dụng onSaveInstanceState() và thay vào đó, sẽ 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 hệ thống khởi chạy hoạt động, thì bạn có thể sử dụng gói bổ sung thay vì gói onSaveInstanceState(). Để tìm hiểu thêm về ý định bổ sung, hãy xem phần Ý đị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ì đơn giản và gọn nhẹ, bạn có thể chỉ sử dụng onSaveInstanceState() để duy trì dữ liệu trạng thái của mình.

Móc vào trạng thái đã lưu bằng SavedStateRegistry

Bắt đầu với Mảnh 1.1.0 hoặc Hoạt động 1.0.0 phần phụ thuộc chuyển đổi, các 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 móc vào 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 dừng hoạt động đố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 tuỳ chọn chung, sẽ vẫn còn lại 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ừ khi 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 lại sau hoạt động do hệ thống khởi tạo và sau khi ứng dụng dừng 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 nếu bạn 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 phần Hướng dẫn 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ì dữ liệu mô hình ứng dụng lâu dài (ví dụ: Qua các lần khởi động lại thiết bị).

Quá trình 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 các loại cơ chế cố định khác nhau. 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 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à toàn thời gian

  • Tính năng cố định cục bộ: Lưu trữ tất cả dữ liệu mà bạn không muốn để mất nếu bạn 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 thiết để hiển thị Trình điều khiển giao diện người dùng liên kết.
    • Ví dụ: Các đối tượng bài hát của tìm kiếm gần đây nhất và cụm từ tìm kiếm gần đây nhất.
  • onSaveInstanceState(): Lưu trữ một lượng dữ liệu nhỏ cần thiết để dễ dàng tải lại trạng thái hoạt động nếu hệ thống dừng rồi tạo lại Trình điều khiển 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ì các đối tượng phức tạp trong bộ nhớ cục bộ và lưu trữ một mã nhận dạng duy nhất cho các đối tượng này trong onSaveInstanceState()
    • 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 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, mọi dữ liệu bài hát phức tạp mà bạn tải từ cơ sở dữ liệu cho Trình điều khiển giao diện người dùng sẽ được lưu trữ ngay lập tức trong đối tượng ViewModel. Bạn cũng nên lưu chính cụm từ tìm kiếm đó trong đối tượng ViewModel.

Khi hoạt động chuyển sang thực hiện trong nền, hệ thống sẽ gọi onSaveInstanceState(). Bạn nên lưu cụm từ tìm kiếm vào gói onSaveInstanceState(). Việc lưu trữ lượng dữ liệu nhỏ này rất dễ dàng thực hiện. Đây cũng là tất cả thông tin bạn cần để đưa hoạt động trở về trạng thái hiện tại.

Quá trình khôi phục các trạng thái phức tạp: Tập hợp các chi tiết

Khi người dùng quay lại hoạt động, có thể có hai trường hợp tạo lại hoạt động sau đây:

  • Hoạt động được tạo lại sau khi hệ thống đã dừng hoạt động. Hoạt động lưu truy vấn trong một gói onSaveInstanceState() và sẽ truyền truy vấn đó sang ViewModel. ViewModel thấy rằng lớp này 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. Hoạt động có truy vấn được lưu trong một gói onSaveInstanceState()ViewModel đã lưu kết quả tìm kiếm vào bộ nhớ đệm. Bạn truyền truy vấn từ gói onSaveInstanceState() sang ViewModel, lớp này xác định rằng lớp đã tải dữ liệu cần thiết 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, hãy xem các tài nguyên sau.

Blog