Lưu lựa chọn ưu tiên trên thiết bị bằng DataStore

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

Giới thiệu

Trong phần này, bạn đã học cách sử dụng SQL và Room để lưu dữ liệu trên thiết bị. SQL và Room đều là những công cụ mạnh mẽ. Tuy nhiên, trong trường hợp bạn không cần lưu trữ dữ liệu quan hệ, DataStore có thể cung cấp một giải pháp đơn giản. Thành phần DataStore Jetpack là một cách tuyệt vời để lưu trữ các tập dữ liệu nhỏ và đơn giản với chi phí thấp. Có hai phương thức triển khai DataStore là Preferences DataStoreProto DataStore.

  • Preferences DataStore lưu trữ các cặp khoá-giá trị. Giá trị có thể là các loại dữ liệu cơ bản của Kotlin, chẳng hạn như String, BooleanInteger. Phương thức này không thể lưu trữ các tập dữ liệu phức tạp. Phương thức này cũng không yêu cầu giản đồ xác định trước. Thường thì Preferences Datastore dùng để lưu trữ lựa chọn ưu tiên của người dùng trên thiết bị của họ.
  • Proto DataStore lưu trữ các loại dữ liệu tuỳ chỉnh. Phương thức này yêu cầu một giản đồ được xác định trước để ánh xạ các định nghĩa proto với cấu trúc đối tượng.

Lớp học lập trình này chỉ đề cập tới Preferences DataStore, nhưng bạn có thể đọc thêm về Proto DataStore trong tài liệu về DataStore.

Preferences DataStore là một cách tuyệt vời để lưu trữ các chế độ cài đặt do người dùng kiểm soát. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách triển khai DataStore để thực hiện chính xác điều đó!

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

Bạn cần có

  • Máy tính có kết nối Internet và Android Studio.
  • Một thiết bị hoặc trình mô phỏng
  • Mã khởi đầu cho ứng dụng Dessert Release

Sản phẩm bạn sẽ tạo ra

Ứng dụng Dessert Release cho thấy một danh sách bản phát hành Android. Biểu tượng trên thanh ứng dụng dùng để chuyển đổi bố cục giữa khung hiển thị lưới và khung hiển thị danh sách.

7f93523c9844380f.png 5b542bebfbd7a3a1.png

Ứng dụng sẽ không lưu bố cục được chọn lúc này. Khi bạn đóng ứng dụng, bố cục bạn chọn sẽ không được lưu và chế độ cài đặt dành cho bố cục sẽ quay về lựa chọn mặc định. Trong lớp học lập trình này, bạn sẽ thêm DataStore vào ứng dụng Dessert Release và sử dụng giải pháp lưu trữ dữ liệu này để lưu trữ các lựa chọn ưu tiên cho bố cục.

2. Tải mã khởi đầu xuống

Nhấp vào đường liên kết sau đây để tải toàn bộ mã cho lớp học lập trình này:

Hoặc nếu muốn, bạn có thể sao chép mã Dessert Release trên GitHub:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout starter
  1. Trong Android Studio, hãy mở thư mục basic-android-kotlin-compose-training-dessert-release.
  2. Mở mã ứng dụng Dessert Release trong Android Studio.

3. Thiết lập phần phụ thuộc

Thêm các dòng sau vào dependencies trong tệp app/build.gradle.kts:

implementation("androidx.datastore:datastore-preferences:1.0.0")

4. Triển khai kho lưu trữ các lựa chọn ưu tiên của người dùng

  1. Trong gói data, hãy tạo một lớp mới có tên là UserPreferencesRepository.

adf3ec7481163f56.png

  1. Trong hàm khởi tạo UserPreferencesRepository, hãy xác định một thuộc tính mang giá trị riêng tư để biểu thị một thực thể đối tượng DataStore có loại Preferences.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

DataStore lưu trữ các cặp khoá-giá trị. Để truy cập vào một giá trị, bạn phải xác định khoá.

  1. Tạo companion object bên trong lớp UserPreferencesRepository.
  2. Dùng hàm booleanPreferencesKey() để xác định một khoá và đặt tên cho khoá đó là is_linear_layout. Tương tự như tên bảng SQL, khoá cần sử dụng định dạng dấu gạch dưới. Khoá này dùng để truy cập vào một giá trị boolean cho biết có nên hiển thị bố cục tuyến tính hay không.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
    private companion object {
        val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    }
    ...
}

Ghi vào DataStore

Bạn tạo và sửa đổi các giá trị trong DataStore bằng cách truyền một hàm lambda vào phương thức edit(). Hàm lambda được truyền một thực thể của MutablePreferences. Bạn có thể sử dụng thực thể này để cập nhật các giá trị trong DataStore. Tất cả nội dung cập nhật bên trong hàm lambda này được thực thi dưới dạng một giao tác duy nhất. Nói cách khác, bản cập nhật này có tính không thể phân chia (atomic) — toàn bộ diễn ra cùng lúc. Loại cập nhật này ngăn chặn trường hợp một số giá trị cập nhật nhưng một số khác thì không.

  1. Tạo một hàm tạm ngưng và gọi hàm này là saveLayoutPreference().
  2. Trong hàm saveLayoutPreference(), hãy gọi phương thức edit() trên đối tượng dataStore.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. Để mã của bạn dễ đọc hơn, hãy định nghĩa tên cho MutablePreferences được cung cấp trong phần thân hàm lambda. Sử dụng thuộc tính đó để đặt giá trị bằng khoá đã xác định và truyền giá trị boolean vào hàm saveLayoutPreference().
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit { preferences ->
        preferences[IS_LINEAR_LAYOUT] = isLinearLayout
    }
}

Lấy dữ liệu từ DataStore

Giờ đây khi bạn đã tạo được cách ghi isLinearLayout vào dataStore, hãy làm theo các bước sau để đọc dữ liệu:

  1. Tạo một thuộc tính trong UserPreferencesRepository thuộc kiểu Flow<Boolean> tên là isLinearLayout.
val isLinearLayout: Flow<Boolean> =
  1. Bạn có thể dùng thuộc tính DataStore.data để hiển thị các giá trị DataStore. Đặt isLinearLayout thành thuộc tính data của đối tượng DataStore.
val isLinearLayout: Flow<Boolean> = dataStore.data

Thuộc tính dataFlow của đối tượng Preferences. Đối tượng Preferences chứa tất cả các cặp khoá-giá trị trong DataStore. Mỗi lần cập nhật dữ liệu trong DataStore, một đối tượng Preferences mới sẽ được phát vào Flow.

  1. Dùng hàm ánh xạ để chuyển đổi Flow<Preferences> thành Flow<Boolean>.

Hàm này chấp nhận tham số lambda với đối tượng Preferences hiện tại làm tham số. Bạn có thể chỉ định khoá mà bạn đã xác định trước đó để có được lựa chọn ưu tiên về bố cục. Xin lưu ý rằng giá trị này có thể không tồn tại nếu saveLayoutPreference chưa được gọi, vì vậy, bạn nên cung cấp một giá trị mặc định.

  1. Chỉ định true để đặt mặc định là thành phần hiển thị bố cục tuyến tính.
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}

Xử lý ngoại lệ

Bất cứ khi nào bạn tương tác với hệ thống tệp trên một thiết bị đều có khả năng xảy ra lỗi. Ví dụ: một tệp có thể không tồn tại hoặc ổ đĩa có thể bị đầy hoặc ngắt kết nối. Khi DataStore đọc và ghi dữ liệu từ các tệp, IOExceptions có thể xảy ra khi truy cập vào DataStore. Hãy sử dụng toán tử catch{} để phát hiện các ngoại lệ và xử lý những lỗi này.

  1. Trong đối tượng đồng hành, hãy triển khai một thuộc tính chuỗi TAG không thể thay đổi để dùng để ghi nhật ký.
private companion object {
    val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    const val TAG = "UserPreferencesRepo"
}
  1. Preferences DataStore gửi một IOException nếu xảy ra lỗi trong khi đọc dữ liệu. Trong khối khởi động isLinearLayout, trước map(), hãy dùng toán tử catch{} để phát hiện IOException.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. Trong khối catch, nếu có IOexception, hãy ghi lỗi và phát emptyPreferences(). Nếu một loại ngoại lệ khác được gửi, hãy ưu tiên loại bỏ ngoại lệ đó. Bằng cách phát emptyPreferences() nếu có lỗi, hàm ánh xạ vẫn có thể ánh xạ tới giá trị mặc định.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {
        if(it is IOException) {
            Log.e(TAG, "Error reading preferences.", it)
            emit(emptyPreferences())
        } else {
            throw it
        }
    }
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }

5. Khởi chạy DataStore

Trong lớp học lập trình này, bạn phải xử lý quá trình chèn phần phụ thuộc theo cách thủ công. Do đó, bạn cần cung cấp Preferences DataStore cho lớp UserPreferencesRepository. Làm theo các bước sau để chèn DataStore vào UserPreferencesRepository.

  1. Tìm gói dessertrelease.
  2. Trong thư mục này, hãy tạo một lớp mới có tên là DessertReleaseApplication và triển khai lớp Application. Đây là vùng chứa cho DataStore của bạn.
class DessertReleaseApplication: Application() {
}
  1. Hãy khai báo private const val có tên là LAYOUT_PREFERENCE_NAME vào bên trong tệp DessertReleaseApplication.kt, nhưng ở bên ngoài lớp DessertReleaseApplication.
  2. Chỉ định giá trị chuỗi layout_preferences cho biến LAYOUT_PREFERENCE_NAME. Sau đó, bạn có thể dùng giá trị này làm tên của Preferences Datastore mà bạn tạo thực thể ở bước tiếp theo.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. Hãy tạo một thuộc tính mang giá trị riêng tư thuộc kiểu DataStore<Preferences> có tên là Context.dataStore ở bên ngoài phần thân lớp DessertReleaseApplication, nhưng nằm trong tệp DessertReleaseApplication.kt bằng cách sử dụng lớp uỷ quyền preferencesDataStore. Truyền LAYOUT_PREFERENCE_NAME cho tham số name của uỷ quyền preferencesDataStore.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)
  1. Bên trong thân lớp DessertReleaseApplication, tạo một thực thể lateinit var cho UserPreferencesRepository.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository
}
  1. Ghi đè phương thức onCreate().
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
    }
}
  1. Bên trong phương thức onCreate(), hãy khởi chạy userPreferencesRepository bằng cách tạo UserPreferencesRepositorydataStore làm tham số.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
        userPreferencesRepository = UserPreferencesRepository(dataStore)
    }
}
  1. Thêm dòng sau vào bên trong thẻ <application> trong tệp AndroidManifest.xml.
<application
    android:name=".DessertReleaseApplication"
    ...
</application>

Phương pháp này xác định lớp DessertReleaseApplication làm điểm truy cập của ứng dụng. Mục đích của mã này là khởi tạo các phần phụ thuộc được xác định trong lớp DessertReleaseApplication trước khi chạy MainActivity.

6. Sử dụng UserPreferencesRepository

Cung cấp kho lưu trữ cho ViewModel

UserPreferencesRepository hiện có sẵn thông qua tính năng chèn phần phụ thuộc, bạn có thể sử dụng tính năng này trong DessertReleaseViewModel.

  1. Trong DessertReleaseViewModel, hãy tạo một thuộc tính UserPreferencesRepository làm tham số hàm khởi tạo.
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. Trong đối tượng đồng hành của ViewModel, thuộc khối viewModelFactory initializer, lấy một thực thể của DessertReleaseApplication bằng mã sau.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. Tạo một thực thể của DessertReleaseViewModel và truyền userPreferencesRepository.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                DessertReleaseViewModel(application.userPreferencesRepository)
            }
        }
    }
}

ViewModel nay có thể truy cập UserPreferencesRepository. Bước tiếp theo là sử dụng khả năng đọc và ghi của UserPreferencesRepository mà bạn đã triển khai trước đó.

Lưu trữ tuỳ chọn bố cục

  1. Chỉnh sửa hàm selectLayout() trong DessertReleaseViewModel để truy cập vào kho lưu trữ tuỳ chọn và cập nhật tuỳ chọn bố cục.
  2. Hãy nhớ rằng việc ghi vào DataStore được thực hiện không đồng bộ bằng hàm suspend. Bắt đầu một Coroutine mới để gọi hàm saveLayoutPreference() của kho lưu trữ tuỳ chọn.
fun selectLayout(isLinearLayout: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.saveLayoutPreference(isLinearLayout)
    }
}

Đọc tuỳ chọn bố cục

Ở phần này, bạn sẽ tái cấu trúc uiState: StateFlow hiện có trong ViewModel để phản ánh isLinearLayout: Flow từ kho lưu trữ.

  1. Xoá mã khởi tạo thuộc tính uiState thành MutableStateFlow(DessertReleaseUiState).
val uiState: StateFlow<DessertReleaseUiState> =

Lựa chọn bố cục tuyến tính ưu tiên từ kho lưu trữ có thể có hai giá trị, đúng hoặc sai, dưới dạng Flow<Boolean>. Giá trị này phải ánh xạ đến một trạng thái giao diện người dùng.

  1. Đặt StateFlow thành kết quả của phép biến đổi tập hợp map() được gọi trên isLinearLayout Flow.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. Trả về một phiên bản của lớp dữ liệu DessertReleaseUiState, truyền isLinearLayout Boolean. Màn hình sử dụng trạng thái giao diện người dùng này để xác định các chuỗi và biểu tượng phù hợp để hiển thị.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }

UserPreferencesRepository.isLinearLayout là một Flow lạnh. Tuy nhiên, để cung cấp trạng thái cho giao diện người dùng, bạn nên sử dụng quy trình nóng, chẳng hạn như StateFlow để trạng thái luôn có sẵn cho giao diện người dùng.

  1. Dùng hàm stateIn() để chuyển đổi Flow thành StateFlow.
  2. Hàm stateIn() chấp nhận 3 tham số: scope, startedinitialValue. Lần lượt truyền viewModelScope, SharingStarted.WhileSubscribed(5_000)DessertReleaseUiState() cho các tham số này.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }
.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = DessertReleaseUiState()
    )
  1. Khởi chạy ứng dụng. Lưu ý rằng bạn có thể nhấp vào biểu tượng bật/tắt để chuyển đổi giữa bố cục lưới và bố cục tuyến tính.

7f93523c9844380f.png 5b542bebfbd7a3a1.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 bố cục ưu tiên của người dùng.

7. Lấy mã giải pháp

Để tải mã này xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng các lệnh git sau:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout main

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip rồi giải nén và mở trong Android Studio.

Nếu bạn muốn xem mã giải pháp, hãy xem mã đó trên GitHub.