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 DataStore
và Proto 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
,Boolean
vàInteger
. 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:
- Hoàn thành bài tập Khái niệm cơ bản về Android với Compose thông qua lớp học lập trình Đọc và cập nhật dữ liệu bằng Room.
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 chế độ xem lưới và chế độ xem danh sách.
Ứ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
- Trong Android Studio, hãy mở thư mục
basic-android-kotlin-compose-training-dessert-release
. - 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
- Trong gói
data
, hãy tạo một lớp mới có tên làUserPreferencesRepository
.
- 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ượngDataStore
có loạiPreferences
.
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á.
- Tạo
companion object
bên trong lớpUserPreferencesRepository
. - 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.
- Tạo một hàm tạm ngưng và gọi hàm này là
saveLayoutPreference()
. - Trong hàm
saveLayoutPreference()
, hãy gọi phương thứcedit()
trên đối tượngdataStore
.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit {
}
}
- Để 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àmsaveLayoutPreference()
.
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:
- Tạo một thuộc tính trong
UserPreferencesRepository
thuộc kiểuFlow<Boolean>
tên làisLinearLayout
.
val isLinearLayout: Flow<Boolean> =
- Bạn có thể dùng thuộc tính
DataStore.data
để hiển thị các giá trịDataStore
. ĐặtisLinearLayout
thành thuộc tínhdata
của đối tượngDataStore
.
val isLinearLayout: Flow<Boolean> = dataStore.data
Thuộc tính data
là Flow
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
.
- Dùng hàm ánh xạ để chuyển đổi
Flow<Preferences>
thànhFlow<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.
- 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.
- 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"
}
Preferences DataStore
gửi mộtIOException
nếu xảy ra lỗi trong khi đọc dữ liệu. Trong khối khởi độngisLinearLayout
, trướcmap()
, hãy dùng toán tửcatch{}
để phát hiệnIOException
.
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
- Trong khối catch, nếu có
IOexception
, hãy ghi lỗi và phátemptyPreferences()
. 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átemptyPreferences()
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
.
- Tìm gói
dessertrelease
. - 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ớpApplication
. Đây là vùng chứa cho DataStore của bạn.
class DessertReleaseApplication: Application() {
}
- Hãy khai báo
private const val
có tên làLAYOUT_PREFERENCE_NAME
vào bên trong tệpDessertReleaseApplication.kt
, nhưng ở bên ngoài lớpDessertReleaseApplication
. - Chỉ định giá trị chuỗi
layout_preferences
cho biếnLAYOUT_PREFERENCE_NAME
. Sau đó, bạn có thể dùng giá trị này làm tên củaPreferences Datastore
mà bạn tạo thực thể ở bước tiếp theo.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
- 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ớpDessertReleaseApplication
, nhưng nằm trong tệpDessertReleaseApplication.kt
bằng cách sử dụng lớp uỷ quyềnpreferencesDataStore
. TruyềnLAYOUT_PREFERENCE_NAME
cho tham sốname
của uỷ quyềnpreferencesDataStore
.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
- Bên trong thân lớp
DessertReleaseApplication
, tạo một thực thểlateinit var
choUserPreferencesRepository
.
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
}
- 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()
}
}
- Bên trong phương thức
onCreate()
, hãy khởi chạyuserPreferencesRepository
bằng cách tạoUserPreferencesRepository
códataStore
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)
}
}
- Thêm dòng sau vào bên trong thẻ
<application>
trong tệpAndroidManifest.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
.
- Trong
DessertReleaseViewModel
, hãy tạo một thuộc tínhUserPreferencesRepository
làm tham số hàm khởi tạo.
class DessertReleaseViewModel(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
...
}
- Trong đối tượng đồng hành của
ViewModel
, thuộc khốiviewModelFactory initializer
, lấy một thực thể củaDessertReleaseApplication
bằng mã sau.
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
...
}
}
}
}
- Tạo một thực thể của
DessertReleaseViewModel
và truyềnuserPreferencesRepository
.
...
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
- Chỉnh sửa hàm
selectLayout()
trongDessertReleaseViewModel
để truy cập vào kho lưu trữ tuỳ chọn và cập nhật tuỳ chọn bố cục. - Hãy nhớ rằng việc ghi vào
DataStore
được thực hiện không đồng bộ bằng hàmsuspend
. Bắt đầu một Coroutine mới để gọi hàmsaveLayoutPreference()
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ữ.
- Xoá mã khởi tạo thuộc tính
uiState
thànhMutableStateFlow(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.
- Đặt
StateFlow
thành kết quả của phép biến đổi tập hợpmap()
được gọi trênisLinearLayout Flow
.
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
- Trả về một phiên bản của lớp dữ liệu
DessertReleaseUiState
, truyềnisLinearLayout 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.
- Dùng hàm
stateIn()
để chuyển đổiFlow
thànhStateFlow
. - Hàm
stateIn()
chấp nhận 3 tham số:scope
,started
vàinitialValue
. Lần lượt truyềnviewModelScope
,SharingStarted.WhileSubscribed(5_000)
và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()
)
- 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.
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.