DataStore Một phần của Android Jetpack.
Jetpack DataStore là một giải pháp lưu trữ dữ liệu cho phép bạn lưu trữ các cặp khoá-giá trị hoặc đối tượng đã nhập có vùng đệm giao thức. DataStore sử dụng coroutine Kotlin và Luồng (Flow) để lưu trữ dữ liệu một cách không đồng bộ, nhất quán và có thể chia sẻ.
Nếu bạn hiện đang sử dụng SharedPreferences
để lưu trữ dữ liệu, hãy cân nhắc chuyển sang DataStore.
DataStore Preference và DataStore Proto
Có hai phương thức triển khai DataStore: DataStore Preference và DataStore Proto.
- Preference DataStore lưu trữ và truy cập vào dữ liệu bằng các khoá. Phương thức triển khai này không yêu cầu có giản đồ được xác định trước và không đảm bảo an toàn về kiểu.
- Proto DataStore lưu trữ dữ liệu theo một kiểu dữ liệu tuỳ chỉnh. Phương thức triển khai này yêu cầu bạn xác định một giản đồ bằng cách sử dụng vùng đệm giao thức, nhưng có đảm bảo an toàn về kiểu.
Sử dụng DataStore đúng cách
Để sử dụng DataStore đúng cách, hãy luôn ghi nhớ các quy tắc sau:
Tuyệt đối không tạo nhiều thực thể của
DataStore
cho một tệp nhất định trong cùng một quy trình. Làm như vậy có thể phá vỡ tất cả chức năng của DataStore. Nếu có nhiều DataStore đang hoạt động cho một tệp nhất định trong cùng một quy trình, thì DataStore sẽ gửiIllegalStateException
khi đọc hoặc cập nhật dữ liệu.Không được thay đổi loại dữ liệu chung của DataStore. Thay đổi kiểu dữ liệu dùng trong DataStore sẽ vô hiệu hoá mọi đảm bảo mà DataStore cung cấp và có thể tạo ra các lỗi nghiêm trọng và khó phát hiện. Bạn nên sử dụng vùng đệm giao thức – một cơ chế đảm bảo tính bất biến, có một API đơn giản và quá trình chuyển đổi tuần tự hiệu quả.Tuyệt đối không sử dụng kết hợp
SingleProcessDataStore
vàMultiProcessDataStore
cho cùng một tệp. Nếu bạn có ý định truy cập vàoDataStore
từ nhiều quy trình, hãy luôn sử dụngMultiProcessDataStore
.
Thiết lập
Để sử dụng Jetpack DataStore trong ứng dụng, hãy thêm đoạn mã sau vào tệp Gradle tuỳ thuộc vào phương thức triển khai mà bạn muốn dùng:
Preferences DataStore
Groovy
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.1" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.1" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.1") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.1") }
Proto DataStore
Groovy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.1" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.1" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.1" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.1") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.1") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.1") }
Lưu trữ các cặp khoá-giá trị với Preferences DataStore
Phương thức triển khai Bộ tuỳ chọn DataStore sử dụng các lớp DataStore
và Preferences
để lưu trữ các cặp khoá-giá trị đơn giản vào ổ đĩa.
Tạo một DataStore Preference
Hãy sử dụng tính năng uỷ quyền thuộc tính do preferencesDataStore
tạo để tạo một bản sao của DataStore<Preferences>
. Gọi tệp kotlin của bạn một lần ở cấp cao nhất và truy cập tệp thông qua thuộc tính này trong suốt phần còn lại của ứng dụng. Thao tác này giúp bạn dễ dàng giữ DataStore
ở dạng một singleton. Bên cạnh đó, hãy sử dụng RxPreferenceDataStoreBuilder
nếu bạn đang dùng RxJava. Tham số bắt buộc name
là tên của DataStore Preference.
Kotlin
// At the top level of your kotlin file: val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
Java
RxDataStore<Preferences> dataStore = new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();
Đọc trong một DataStore Preference
Do DataStore Preference không sử dụng giản đồ được xác định trước, bạn phải sử dụng hàm loại khoá tương ứng để xác định khoá cho mỗi giá trị mà bạn cần lưu trữ trong phiên bản DataStore<Preferences>
. Ví dụ: để xác định khoá cho một giá trị int, hãy sử dụng intPreferencesKey()
.
Sau đó, dùng thuộc tính DataStore.data
để hiển thị giá trị được lưu trữ phù hợp bằng Flow
.
Kotlin
val EXAMPLE_COUNTER = intPreferencesKey("example_counter") val exampleCounterFlow: Flow<Int> = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0 }
Java
Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter"); Flowable<Integer> exampleCounterFlow = dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));
Ghi vào một DataStore Preference
DataStore Preference cung cấp một hàm edit()
có thể chia sẻ cập nhật dữ liệu trong một DataStore
. Tham số transform
của hàm chấp nhận một khối mã để bạn có thể cập nhật các giá trị nếu cần. Tất cả mã trong khối biến đổi được coi là một lượt chia sẻ.
Kotlin
suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 } }
Java
Single<Preferences> updateResult = dataStore.updateDataAsync(prefsIn -> { MutablePreferences mutablePreferences = prefsIn.toMutablePreferences(); Integer currentInt = prefsIn.get(INTEGER_KEY); mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1); return Single.just(mutablePreferences); }); // The update is completed once updateResult is completed.
Lưu trữ các đối tượng đã nhập bằng DataStore Proto
Phương thức triển khai DataStore Proto sử dụng DataStore và các bộ đệm giao thức để lưu trữ các đối tượng đã nhập vào ổ đĩa.
Xác định một giản đồ
DataStore Proto yêu cầu một giản đồ được xác định trước trong tệp proto trong thư mục app/src/main/proto/
. Giản đồ này xác định loại cho các đối tượng mà bạn lưu trữ trong DataStore Proto. Để tìm hiểu thêm về cách xác định một giản đồ proto, hãy xem hướng dẫn về ngôn ngữ protobuf.
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Tạo một DataStore Proto
Bạn cần thực hiện hai bước để tạo một DataStore Proto cho việc lưu trữ các đối tượng đã nhập:
- Xác định một lớp triển khai
Serializer<T>
, trong đóT
là loại được xác định trong tệp proto. Lớp chuyển đổi tuần tự này cho DataStore biết cách đọc và ghi loại dữ liệu của bạn. Hãy đảm bảo bạn cung cấp giá trị mặc định để trình chuyển đổi tuần tự sử dụng nếu chưa có tệp nào được tạo. - Sử dụng tính năng uỷ quyền thuộc tính do
dataStore
tạo để tạo một bản sao củaDataStore<T>
, trong đóT
là loại được xác định trong tệp proto. Hãy gọi tệp kotlin của bạn một lần ở cấp cao nhất và truy cập tệp thông qua tính năng uỷ quyền thuộc tính này trong suốt thời gian còn lại của ứng dụng. Tham sốfilename
cho DataStore biết nên sử dụng tệp nào để lưu trữ dữ liệu, còn tham sốserializer
cho DataStore biết tên của lớp chuyển đổi tuần tự đã xác định trong bước 1.
Kotlin
object SettingsSerializer : Serializer<Settings> { override val defaultValue: Settings = Settings.getDefaultInstance() override suspend fun readFrom(input: InputStream): Settings { try { return Settings.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo( t: Settings, output: OutputStream) = t.writeTo(output) } val Context.settingsDataStore: DataStore<Settings> by dataStore( fileName = "settings.pb", serializer = SettingsSerializer )
Java
private static class SettingsSerializer implements Serializer<Settings> { @Override public Settings getDefaultValue() { Settings.getDefaultInstance(); } @Override public Settings readFrom(@NotNull InputStream input) { try { return Settings.parseFrom(input); } catch (exception: InvalidProtocolBufferException) { throw CorruptionException(“Cannot read proto.”, exception); } } @Override public void writeTo(Settings t, @NotNull OutputStream output) { t.writeTo(output); } } RxDataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();
Đọc trong một DataStore Proto
Hãy sử dụng DataStore.data
để hiển thị Flow
của thuộc tính phù hợp trong đối tượng được lưu trữ của bạn.
Kotlin
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter }
Java
Flowable<Integer> exampleCounterFlow = dataStore.data().map(settings -> settings.getExampleCounter());
Ghi vào một DataStore Proto
DataStore Proto cung cấp một
hàm updateData()
có thể chia sẻ cập nhật đối tượng được lưu trữ. updateData()
cho bạn biết
trạng thái hiện tại của dữ liệu làm phiên bản cho loại dữ liệu và cập nhật
dữ liệu có thể chia sẻ trong thao tác đọc-ghi-sửa đổi nguyên tử.
Kotlin
suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() } }
Java
Single<Settings> updateResult = dataStore.updateDataAsync(currentSettings -> Single.just( currentSettings.toBuilder() .setExampleCounter(currentSettings.getExampleCounter() + 1) .build()));
Sử dụng DataStore trong mã đồng bộ
Một trong những lợi ích chính của DataStore là API không đồng bộ, nhưng không phải lúc nào bạn cũng có thể thay đổi mã xung quanh thành không đồng bộ. Điều này có thể xảy ra trong trường hợp bạn đang xử lý một cơ sở mã hiện có sử dụng ổ đĩa I/O đồng bộ hoặc nếu bạn có một phần phụ thuộc không cung cấp API không đồng bộ.
Coroutine của Kotlin cung cấp trình tạo coroutine runBlocking()
để giúp thu hẹp khoảng cách giữa mã đồng bộ và không đồng bộ. Bạn có thể sử dụng runBlocking()
để đọc dữ liệu trong DataStore một cách đồng bộ.
RxJava cung cấp các phương thức chặn trên Flowable
. Mã sau đây chặn luồng gọi cho đến khi DataStore trả về dữ liệu:
Kotlin
val exampleData = runBlocking { context.dataStore.data.first() }
Java
Settings settings = dataStore.data().blockingFirst();
Việc thực hiện các thao tác I/O đồng bộ trên luồng giao diện người dùng có thể khiến ANR hoặc giao diện người dùng bị giật. Bạn có thể giảm thiểu những vấn đề này bằng cách tải trước không đồng bộ dữ liệu trong DataStore:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. } }
Java
dataStore.data().first().subscribe();
Bằng cách này, DataStore đọc không đồng bộ dữ liệu và lưu dữ liệu vào bộ nhớ đệm trong bộ nhớ. Các lượt đọc đồng bộ sau này sử dụng runBlocking()
có thể nhanh hơn hoặc có thể tránh được toàn bộ thao tác của ổ đĩa I/O nếu lần đọc ban đầu hoàn tất.
Sử dụng DataStore trong mã đa quá trình
Bạn có thể định cấu hình DataStore để truy cập vào cùng một dữ liệu trong nhiều quy trình với sự đảm bảo về tính nhất quán của dữ liệu như trong một quy trình. Cụ thể, DataStore đảm bảo:
- Hoạt động đọc chỉ trả về dữ liệu đã lưu trữ trên ổ đĩa.
- Tính nhất quán đọc sau khi ghi.
- Hoạt động ghi được chuyển đổi tuần tự.
- Hoạt động đọc tuyệt đối không bị chặn bởi hoạt động ghi.
Hãy xem xét một ứng dụng mẫu có dịch vụ và hoạt động:
Dịch vụ đang chạy trong một quy trình riêng và định kỳ cập nhật DataStore
<service android:name=".MyService" android:process=":my_process_id" />
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { scope.launch { while(isActive) { dataStore.updateData { Settings(lastUpdate = System.currentTimeMillis()) } delay(1000) } } }
Đồng thời, ứng dụng sẽ thu thập những thay đổi đó và cập nhật giao diện người dùng
val settings: Settings by dataStore.data.collectAsState() Text( text = "Last updated: $${settings.timestamp}", )
Để có thể sử dụng DataStore trong nhiều quy trình, bạn cần xây dựng đối tượng DataStore bằng MultiProcessDataStoreFactory
.
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
serializer
cho DataStore biết cách đọc và ghi loại dữ liệu của bạn.
Hãy đảm bảo rằng bạn cung cấp giá trị mặc định để trình chuyển đổi tuần tự sử dụng nếu chưa có tệp nào được tạo. Dưới đây là một ví dụ về phương thức triển khai bằng kotlinx.serialization:
@Serializable
data class Settings(
val lastUpdate: Long
)
@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {
override val defaultValue = Settings(lastUpdate = 0)
override suspend fun readFrom(input: InputStream): Timer =
try {
Json.decodeFromString(
Settings.serializer(), input.readBytes().decodeToString()
)
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read Settings", serialization)
}
override suspend fun writeTo(t: Settings, output: OutputStream) {
output.write(
Json.encodeToString(Settings.serializer(), t)
.encodeToByteArray()
)
}
}
Bạn có thể sử dụng tính năng chèn phần phụ thuộc Hilt để đảm bảo thực thể DataStore là duy nhất cho mỗi quy trình:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
Xử lý tệp bị hỏng
Trong một số trường hợp hiếm hoi, tệp lưu trữ cố định trên ổ đĩa của DataStore có thể bị lỗi. Theo mặc định, DataStore không tự động khôi phục khi bị hỏng, và việc cố gắng đọc từ DataStore sẽ khiến hệ thống gửi một CorruptionException
.
DataStore cung cấp một API trình xử lý hỏng hóc có thể giúp bạn khôi phục một cách linh hoạt trong trường hợp như vậy và tránh gửi ngoại lệ. Khi được định cấu hình, trình xử lý hỏng sẽ thay thế tệp bị hỏng bằng một tệp mới chứa giá trị mặc định được xác định trước.
Để thiết lập trình xử lý này, hãy cung cấp corruptionHandler
khi tạo phiên bản DataStore trong by dataStore()
hoặc trong phương thức factory DataStoreFactory
:
val dataStore: DataStore<Settings> = DataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
Gửi ý kiến phản hồi
Hãy chia sẻ phản hồi và ý kiến của bạn với chúng tôi thông qua các tài nguyên sau:
- Công cụ theo dõi lỗi
- Báo cáo sự cố để chúng tôi có thể sửa lỗi.
Tài nguyên khác
Để tìm hiểu thêm về Jetpack DataStore, hãy xem thêm các tài nguyên sau:
Mẫu
Blog
Lớp học lập trình
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Tải và hiện dữ liệu được phân trang
- Tổng quan về LiveData
- Bố cục và biểu thức liên kết