Preferences DataStore

1. Trước khi bắt đầu

Trong các lớp học lập trình trước, bạn đã tìm hiểu cách lưu dữ liệu trong cơ sở dữ liệu SQLite bằng Room, một lớp trừu tượng của cơ sở dữ liệu. Lớp học lập trình này giới thiệu về Jetpack DataStore. Được xây dựng trên nền tảng coroutine và Luồng Kotlin, DataStore cung cấp hai cách triển khai khác nhau: Proto DataStore, nơi lưu trữ các đối tượng đã nhập và Preferences DataStore, nơi lưu trữ các cặp giá trị/khoá.

Trong lớp học thực hành lập trình này, bạn sẽ tìm hiểu cách sử dụng Preferences DataStore. Proto DataStore nằm ngoài phạm vi lớp học lập trình này.

Điều kiện tiên quyết

  • Bạn đã quen thuộc với bộ thành phần cấu trúc Android gồm ViewModel, LiveDataFlow cũng như biết cách sử dụng ViewModelProvider.Factory để tạo thực thể ViewModel.
  • Bạn đã nắm rõ kiến thức cơ bản liên quan đến cơ chế xử lý đồng thời.
  • Bạn biết cách sử dụng coroutine cho các tác vụ chạy trong thời gian dài.

Kiến thức bạn sẽ học được

  • DataStore là gì, lý do và thời điểm nên sử dụng DataStore.
  • Cách thêm Preference DataStore vào ứng dụng.

Bạn cần có

  • Đoạn mã khởi đầu cho ứng dụng Words (giống với đoạn mã giải pháp cho ứng dụng Words từ lớp học lập trình trước đó).
  • Máy tính đã cài đặt Android Studio.

Tải đoạn mã khởi đầu xuống cho lớp học lập trình này

Trong lớp học lập trình này, bạn sẽ mở rộng các tính năng của ứng dụng Words từ đoạn mã giải pháp trước đó. Mã khởi đầu cũng có thể chứa đoạn mã mà bạn đã biết từ các lớp học lập trình trước.

Để lấy mã cho lớp học lập trình này trên GitHub và mở trong Android Studio, hãy thực hiện các bước sau.

  1. Khởi động Android Studio.
  2. Trên cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Get from VCS (Lấy trên VCS).

61c42d01719e5b6d.png

  1. Trong hộp thoại Get from Version Control (Lấy từ hệ thống Quản lý phiên bản), bạn hãy nhớ chọn Git cho mục Version control(Quản lý phiên bản).

9284cfbe17219bbb.png

  1. Dán URL đã được cung cấp để lấy đoạn mã vào hộp URL.
  2. Bạn có thể thay đổi thư mục mặc định được đề xuất trong Directory (Thư mục).

5ddca7dd0d914255.png

  1. Nhấp vào Clone (Nhân bản). Android Studio bắt đầu tìm nạp mã của bạn.
  2. Đợi Android Studio tìm nạp xong.
  3. Chọn chính xác mô-đun cho mã khởi đầu, mã nguồn ứng dụng hoặc đoạn mã giải pháp của lớp học này.

2919fe3e0c79d762.png

  1. Nhấp vào nút Run (Chạy) 8de56cba7583251f.png để tạo và chạy đoạn mã của bạn.

2. Tổng quan về ứng dụng khởi đầu

Ứng dụng Words bao gồm hai màn hình: Màn hình đầu tiên hiển thị các chữ cái mà người dùng có thể chọn, màn hình thứ hai hiển thị một danh sách các từ bắt đầu bằng chữ cái đã chọn.

Ứng dụng này có tuỳ chọn trình đơn để người dùng chuyển đổi giữa bố cục danh sách và bố cục lưới gồm các chữ cái.

  1. Tải đoạn mã khởi đầu xuống rồi mở trong Android Studio và chạy ứng dụng. Các chữ cái được hiển thị theo một bố cục tuyến tính.
  2. Nhấn vào tuỳ chọn trình đơn ở góc trên bên phải. Bố cục chuyển sang bố cục Lưới.
  3. Thoát và chạy lại ứng dụng. Bạn có thể thực hiện việc này bằng cách sử dụng các tuỳ chọn Stop 'app' f782441b99bdd0a4.png (Dừng "ứng dụng") và Run 'app' d203bd07cbce5954.png (Chạy "ứng dụng") trong Android Studio. Lưu ý khi ứng dụng được chạy lại, các chữ cái sẽ hiển thị theo bố cục tuyến tính và không hiển thị theo bố cục lưới.

Lưu ý hệ thống không giữ lại lựa chọn của người dùng. Lớp học lập trình này cho bạn biết cách khắc phục vấn đề này.

Ứng dụng bạn sẽ tạo

  • Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng Preferences DataStore để duy trì tuỳ chọn cài đặt bố cục trong DataStore.

3. Giới thiệu về Preferences DataStore

Preferences DataStore là lựa chọn lý tưởng cho các tập dữ liệu nhỏ, đơn giản, chẳng hạn như lưu trữ chi tiết đăng nhập, cài đặt chế độ tối, kích thước phông chữ, v.v. DataStore không phù hợp cho các tập dữ liệu phức tạp, chẳng hạn như danh sách kho hàng của cửa hàng tạp hoá trực tuyến hoặc cơ sở dữ liệu về học viên. Nếu bạn cần lưu trữ các tập dữ liệu lớn hoặc phức tạp, hãy cân nhắc sử dụng Room thay vì DataStore.

Khi sử dụng thư viện Jetpack DataStore, bạn có thể tạo một API đơn giản, an toàn và không đồng bộ để lưu trữ dữ liệu. Tiện ích này cung cấp hai cách triển khai khác nhau: Preferences DataStore và Proto DataStore. Mặc dù cả Preferences DataStore và Proto DataStore đều cho phép tiết kiệm dữ liệu nhưng chúng thực hiện theo hai cách khác nhau:

  • Preferences DataStore truy cập và lưu trữ dữ liệu dựa trên khoá mà không cần xác định trước giản đồ (mô hình cơ sở dữ liệu).
  • Proto DataStore xác định giản đồ bằng Vùng đệm giao thức. Bạn có thể lưu giữ dữ liệu được nhập phần lớn bằng cách sử dụng vùng đệm Giao thức hay Protobufs. Protobufs nhanh hơn, nhỏ hơn, đơn giản hơn và rõ ràng hơn so với XML và các định dạng dữ liệu tương tự khác.

Room so với Datastore: thời điểm sử dụng

Nếu ứng dụng của bạn cần lưu trữ dữ liệu lớn/phức tạp ở định dạng có cấu trúc như SQL, hãy xem xét sử dụng Room. Tuy nhiên, nếu bạn chỉ cần lưu trữ một lượng dữ liệu đơn giản hoặc nhỏ vốn có thể được lưu trong cặp khoá-giá trị, thì DataStore là lựa chọn lý tưởng.

Proto so với Preferences DataStore: thời điểm sử dụng

Proto DataStore an toàn về kiểu và hiệu quả nhưng đòi hỏi phải định cấu hình và thiết lập. Nếu dữ liệu ứng dụng của bạn đủ đơn giản để có thể lưu trong các cặp khoá-giá trị, thì Preferences DataStore là lựa chọn tốt hơn bởi thiết lập dễ dàng hơn nhiều.

Thêm Preferences DataStore dưới dạng phần phụ thuộc

Bước đầu tiên để tích hợp DataStore với ứng dụng là thêm nó dưới dạng phần phụ thuộc.

  1. Trong build.gradle(Module: Words.app), thêm phần phụ thuộc sau:
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

4. Tạo một Preferences DataStore

  1. Thêm một gói có tên data và tạo một lớp Kotlin có tên SettingsDataStore trong đó.
  2. Thêm một tham số hàm khởi tạo vào lớp SettingsDataStore của kiểu Context.
class SettingsDataStore(context: Context) {}
  1. Bên ngoài lớp SettingsDataStore, khai báo private const val có tên là LAYOUT_PREFERENCES_NAME và chỉ định giá trị chuỗi layout_preferences cho lớp đó. Đây là tên của Preferences Datastore mà bạn sẽ tạo ở bước tiếp theo.
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
  1. Vẫn ở bên ngoài lớp trên, hãy tạo một thực thể DataStore bằng cách sử dụng uỷ quyền preferencesDataStore. Do bạn đang sử dụng Preferences Datastore, bạn cần truyền Preferences dưới dạng kiểu kho dữ liệu. Ngoài ra, hãy đặt kho dữ liệu name về LAYOUT_PREFERENCES_NAME.

Đoạn mã hoàn tất:

private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"

// Create a DataStore instance using the preferencesDataStore delegate, with the Context as
// receiver.
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(
   name = LAYOUT_PREFERENCES_NAME
)

5. Triển khai Lớp SettingsDataStore

Như đã đề cập, Preferences DataStore lưu trữ dữ liệu trong các cặp khoá-giá trị. Trong bước này, bạn xác định các khoá bắt buộc để lưu trữ cài đặt bố cục, đồng thời xác định các hàm cần ghi vào và đọc từ Preferences DataStore.

Hàm kiểu khoá

Preferences DataStore không sử dụng giản đồ định trước như Room mà sử dụng các hàm kiểu khoá tương ứng để xác định khoá cho mỗi giá trị mà bạn lưu trữ trong thực thể DataStore<Preferences>. Ví dụ: để xác định khoá cho giá trị int, hãy sử dụng intPreferencesKey() và đối với giá trị string, hãy sử dụng stringPreferencesKey(). Nói chung, các tên hàm này được thêm tiền tố kiểu dữ liệu mà bạn muốn lưu trữ vào khoá.

Triển khai những nội dung sau trong lớp data\SettingsDataStore:

  1. Để triển khai lớp SettingsDataStore, bước đầu tiên là tạo khoá lưu trữ giá trị Boolean chỉ định xem chế độ cài đặt người dùng chọn có phải bố cục tuyến tính hay không. Tạo một thuộc tính lớp private có tên là IS_LINEAR_LAYOUT_MANAGER và khởi chạy thuộc tính này bằng cách truyền booleanPreferencesKey() vào tên khoá is_linear_layout_manager làm tham số hàm.
private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

Ghi vào Preferences DataStore

Đây là lúc sử dụng khoá của bạn và lưu trữ chế độ cài đặt bố cục Boolean trong DataStore. Preferences DataStore cung cấp hàm tạm ngưng edit() cho phép cập nhật có giao dịch dữ liệu trong DataStore. Thông số biến đổi của hàm chấp nhận một khối mã nhờ đó bạn có thể cập nhật các giá trị nếu cần. Tất cả mã trong khối chuyển đổi được coi là một giao dịch duy nhất. Về sau, công việc giao dịch được chuyển sang Dispacter.IO, do đó đừng quên thực hiện hàm suspend khi gọi hàm edit().

  1. Tạo một hàm suspend có tên là saveLayoutToPreferencesStore(). Hàm này có hai tham số: Boolean cài đặt bố cục và Context.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {

}
  1. Triển khai hàm trên, gọi dataStore.edit() và chuyển vào một khối mã để lưu trữ giá trị mới.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
   context.dataStore.edit { preferences ->
       preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
   }
}

Đọc từ Preferences DataStore

Preferences DataStore tiết lộ dữ liệu được lưu trữ trong Flow<Preferences> và xuất mỗi khi tuỳ chọn thay đổi. Bạn không muốn hiển thị toàn bộ đối tượng Preferences mà chỉ hiển thị giá trị Boolean. Để thực hiện việc này, chúng tôi liên kết Flow<Preferences> và nhận giá trị Boolean mà bạn quan tâm.

  1. Hiển thị preferenceFlow: Flow<UserPreferences>, được xây dựng dựa trên dataStore.data: Flow<Preferences>, liên kết nó để truy xuất tuỳ chọn Boolean. Do Datastore trống trong lần chạy đầu tiên, hãy trả về true theo mặc định.
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }
  1. Thêm các mục nhập sau nếu chúng không được nhập tự động:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

Xử lý ngoại lệ

Khi DataStore đọc và ghi dữ liệu từ các tệp, IOExceptions có thể xảy ra khi truy cập dữ liệu. Bạn xử lý các trường hợp này bằng cách sử dụng toán tử catch() để phát hiện các trường hợp ngoại lệ.

  1. SharedPreferences DataStore sẽ tạo ra một IOException nếu xảy ra lỗi trong khi đọc dữ liệu. Trong phần khai báo preferenceFlow, trước map(), dùng toán tử catch() để bắt IOException và phát emptyPreferences(). Nói một cách đơn giản, do chúng tôi không dự kiến sẽ có loại ngoại lệ nào khác ở đây, nên nếu có một loại ngoại lệ khác, hãy loại bỏ chúng.
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .catch {
       if (it is IOException) {
           it.printStackTrace()
           emit(emptyPreferences())
       } else {
           throw it
       }
   }
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }

Lớp data\SettingsDataStore của bạn đã sẵn sàng để sử dụng!

6. Sử dụng Lớp SettingsDataStore

Trong nhiệm vụ tiếp theo, bạn sẽ sử dụng SettingsDataStore trong lớp LetterListFragment. Bạn sẽ đính kèm trình quan sát vào cài đặt bố cục và cập nhật giao diện người dùng cho phù hợp.

Thực hiện các bước sau trong LetterListFragment

  1. Khai báo biến thuộc loại private được gọi là SettingsDataStore thuộc loại SettingsDataStore. Tạo biến này lateinit vì bạn sẽ khởi động biến này sau đó.
private lateinit var SettingsDataStore: SettingsDataStore
  1. Ở cuối hàm onViewCreated(), khởi động biến mới và chuyển biến vào requireContext() đến hàm khởi tạo SettingsDataStore.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
}

Đọc và quan sát dữ liệu

  1. Trong LetterListFragment, bên trong phương thức onViewCreated(), bên dưới phần khởi động SettingsDataStore, chuyển đổi preferenceFlow thành Livedata sử dụng asLiveData(). Đính kèm trình quan sát và chuyển vào viewLifecycleOwner làm chủ sở hữu.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
  1. Bên trong trình quan sát, chỉ định cài đặt bố cục mới cho biến isLinearLayoutManager. Gọi lệnh đến hàm chooseLayout() để cập nhật bố cục RecyclerView.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })

Hàm onViewCreated() đã hoàn tất sẽ có dạng như sau:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })
}

Ghi cài đặt bố cục vào DataStore

Bước cuối cùng là ghi cài đặt bố cục vào Preferences DataStore khi người dùng nhấn vào tuỳ chọn menu. Việc ghi dữ liệu vào Preference Datastore phải được thực hiện không đồng bộ bên trong coroutine. Để thực hiện việc ghi dữ liệu này trong một mảnh, dùng CoroutineScope có tên LifecycleScope.

LifecycleScope

Các thành phần nhận biết vòng đời, chẳng hạn như mảnh, hỗ trợ coroutine hạng nhất cho phạm vi logic trong ứng dụng cùng một lớp có khả năng tương tác với LiveData. LifecycleScope được xác định cho từng đối tượng Lifecycle. Mọi coroutine khởi chạy trong phạm vi này đều bị huỷ khi chủ sở hữu Lifecycle bị phá huỷ.

  1. Trong LetterListFragment, bên trong hàm onOptionsItemSelected(), ở cuối trường hợp R.id.action_switch_layout, chạy coroutine bằng lifecycleScope. Bên trong khối launch, thực hiện lệnh gọi tới saveLayoutToPreferencesStore() chuyển vào isLinearLayoutManagercontext.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           ...
           // Launch a coroutine and write the layout setting in the preference Datastore
           lifecycleScope.launch {
       SettingsDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
           }
           ...

           return true
       }
  1. Chạy ứng dụng. Nhấp vào tuỳ chọn trình đơn để thay đổi bố cục ứng dụng.

  1. Bây giờ hãy kiểm tra tính ổn định của Preferences DataStore. Thay đổi bố cục ứng dụng thành bố cục Lưới. Thoát và chạy lại ứng dụng. Bạn có thể thực hiện việc này bằng cách sử dụng các tuỳ chọn Stop 'app' f782441b99bdd0a4.png (Dừng "ứng dụng") và Run 'app' d203bd07cbce5954.png (Chạy "ứng dụng") trong Android Studio.

cd2c31f27dfb5157.png

Bây giờ, khi ứng dụng đã được chạy lại, các chữ cái sẽ hiển thị theo bố cục Lưới và không hiển thị theo bố cục Tuyến tính. Ứng dụng của bạn đang lưu thành công cài đặt bố cục mà người dùng đã chọn!

Lưu ý mặc dù các chữ cái hiện được hiển thị trong bố cục Lưới, nhưng biểu tượng trình đơn không được cập nhật đúng. Tiếp theo, chúng ta sẽ xem xét cách khắc phục sự cố này.

7. Sửa lỗi biểu tượng trình đơn

Nguyên nhân gây lỗi biểu tượng trình đơn là do trong onViewCreated(), bố cục RecyclerView được cập nhật theo cài đặt bố cục chứ không phải biểu tượng trình đơn. Vấn đề này có thể được giải quyết bằng cách vẽ lại trong trình đơn cùng với việc cập nhật bố cục RecyclerView.

Đang vẽ lại trình đơn tuỳ chọn

Sau khi trình đơn được tạo, bạn không thể vẽ lại mọi khung hình do sẽ không cần thiết phải vẽ lại cùng một trình đơn ở mọi khung hình. Hàm invalidateOptionsMenu() yêu cầu Android vẽ lại trình đơn tuỳ chọn.

Bạn có thể gọi hàm này khi thay đổi nội dung nào đó trong trình đơn Options (Tuỳ chọn), chẳng hạn như thêm một mục menu, xoá một mục hoặc thay đổi văn bản hay biểu tượng menu. Trong trường hợp này, biểu tượng trình đơn đã bị thay đổi. Việc gọi phương thức này sẽ khai báo rằng trình đơn Options (Tuỳ chọn) đã thay đổi, do đó trình đơn này sẽ được tạo lại. Phương thức onCreateOptionsMenu(android.view.Menu) được gọi ở lần cần hiển thị tiếp theo.

  1. LetterListFragment, bên trong onViewCreated(), cuối trình quan sát preferenceFlow, bên dưới cuộc gọi tới chooseLayout(). Vẽ lại trình đơn bằng cách gọi invalidateOptionsMenu() trên activity.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           ...
           // Redraw the menu
           activity?.invalidateOptionsMenu()
   })
}
  1. Chạy lại ứng dụng và thay đổi bố cục.
  2. Thoát và chạy lại ứng dụng. Lưu ý biểu tượng trình đơn hiện đã được cập nhật chính xác.

1c8cf63c8d175aad.png

Xin chúc mừng! Bạn đã thêm thành công Preferences DataStore vào ứng dụng của mình để lưu lựa chọn của người dùng.

8. Mã giải pháp

Mã giải pháp cho lớp học lập trình này nằm trong dự án và mô-đun dưới đây.

9. Tóm tắt

  • DataStore có một API hoàn toàn không đồng bộ sử dụng coroutine và Quy trình Kotlin, đảm bảo tính nhất quán của dữ liệu.
  • Jetpack DataStore là một giải pháp lưu trữ dữ liệu cho phép lưu trữ các cặp khoá-giá trị hoặc đối tượng đã nhập có vùng đệm giao thức.
  • Có hai phương thức triển khai DataStore: DataStore Preference và DataStore Proto.
  • Preferences DataStore không sử dụng giản đồ xác định trước.
  • Preferences DataStore sử dụng hàm loại khoá tương ứng để xác định khoá cho mỗi giá trị bạn cần lưu trữ trong phiên bản DataStore<Preferences>. Ví dụ: để xác định khoá cho giá trị int, sử dụng intPreferencesKey().
  • Preferences DataStore cung cấp hàm edit() cập nhật dữ liệu giao dịch trong DataStore.

10. Tìm hiểu thêm

Blog

Ưu tiên lưu trữ dữ liệu với Jetpack DataStore