DataStore בארגז הכלים Android Jetpack.
מאגר הנתונים Jetpack DataStore מאפשר לכם לאחסן צמדי מפתח-ערך או אובייקטים מוקלדים באמצעות מאגרי אחסון לפרוטוקולים. המאגר משתמש בשגרות המשך וב-Flow של Kotlin כדי לאחסן נתונים באופן אסינכרוני, עקבי וטרנזקציונלי.
אם אתם משתמשים ב-SharedPreferences לאחסון נתונים, כדאי לשקול מעבר ל-DataStore.
DataStore API
ממשק DataStore מספק את ה-API הבא:
תהליך שאפשר להשתמש בו כדי לקרוא נתונים מ-DataStore
val data: Flow<T>פונקציה לעדכון נתונים ב-DataStore
suspend updateData(transform: suspend (t) -> T)
הגדרות של DataStore
אם רוצים לאחסן נתונים ולגשת אליהם באמצעות מפתחות, אפשר להשתמש בהטמעה של Preferences DataStore שלא דורשת סכימה מוגדרת מראש, ולא מספקת מניעת שגיאות הקלדה. יש לו API שדומה ל-SharedPreferences, אבל בלי החסרונות שקשורים להעדפות משותפות.
DataStore מאפשר לכם לשמור מחלקות בהתאמה אישית. כדי לעשות זאת, צריך להגדיר סכימה לנתונים ולספק Serializer כדי להמיר אותם לפורמט שניתן לשמירה. אתם יכולים לבחור להשתמש במאגרי אחסון לפרוטוקולים, ב-JSON או בכל שיטת סריאליזציה אחרת.
הגדרה
כדי להשתמש ב-Jetpack DataStore באפליקציה, מוסיפים את הקוד הבא לקובץ Gradle, בהתאם להטמעה שרוצים להשתמש בה:
DataStore להעדפות
מוסיפים את השורות הבאות לחלק של יחסי התלות בקובץ gradle:
Groovy
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation "androidx.datastore:datastore-preferences:1.2.1" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-preferences-core:1.2.1" }
Kotlin
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation("androidx.datastore:datastore-preferences:1.2.1") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-preferences-core:1.2.1") }
כדי להוסיף תמיכה אופציונלית ב-RxJava, מוסיפים את יחסי התלות הבאים:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.2.1" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.2.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.2.1") }
DataStore
מוסיפים את השורות הבאות לחלק של יחסי התלות בקובץ gradle:
Groovy
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation "androidx.datastore:datastore:1.2.1" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-core:1.2.1" }
Kotlin
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation("androidx.datastore:datastore:1.2.1") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-core:1.2.1") }
לתמיכה ב-RxJava, צריך להוסיף את יחסי התלות האופציונליים הבאים:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.2.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.2.1" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.2.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.2.1") }
כדי לבצע סריאליזציה של תוכן, מוסיפים תלות בסריאליזציה של מאגרי אחסון לפרוטוקולים או של JSON.
סריאליזציה של JSON
כדי להשתמש בסריאליזציה של JSON, מוסיפים את מה שמופיע כאן למטה לקובץ Gradle:
Groovy
plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" }
Kotlin
plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") }
סריאליזציה של Protobuf
כדי להשתמש בסריאליזציה של Protobuf, מוסיפים את מה שמופיע כאן למטה לקובץ Gradle:
Groovy
plugins { id("com.google.protobuf") version "0.9.5" } dependencies { implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1" } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.1" } generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } create("kotlin") } } } }
Kotlin
plugins { id("com.google.protobuf") version "0.9.5" } dependencies { implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1") } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.1" } generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } create("kotlin") } } } }
שימוש נכון ב-DataStore
כדי להשתמש ב-DataStore בצורה נכונה, חשוב לזכור את הכללים הבאים:
לעולם אל תיצרו יותר ממופע אחד של
DataStoreעבור קובץ נתון באותו תהליך. פעולה כזו עלולה לשבש את כל הפונקציונליות של DataStore. אם יש כמה חנויות נתונים פעילות לקובץ נתון באותו תהליך, DataStore יציג שגיאהIllegalStateExceptionכשקוראים או מעדכנים נתונים.הסוג הגנרי של
DataStore<T>חייב להיות בלתי ניתן לשינוי. שינוי של סוג שמשמש ב-DataStore מבטל את העקביות ש-DataStore מספק, ויוצר באגים שעלולים להיות חמורים וקשים לאיתור. מומלץ להשתמש במאגרי אחסון לפרוטוקולים, שעוזרים להבטיח יציבות, API ברור וסריאליזציה יעילה.אל תערבבו בין שימושים במאפיינים
SingleProcessDataStoreו-MultiProcessDataStoreבאותו קובץ. אם אתם מתכוונים לגשת אלDataStoreמתוך יותר מתהליך אחד, אתם צריכים להשתמש ב-MultiProcessDataStore.
הגדרת נתונים
Preferences DataStore
הגדרת מפתח שישמש לשמירת נתונים בדיסק.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
JSON DataStore
במאגר נתונים של JSON, מוסיפים הערה @Serialization לנתונים שרוצים לשמור.
@Serializable
data class Settings(
val exampleCounter: Int
)
מגדירים מחלקה שמטמיעה את Serializer<T>, כאשר T הוא הסוג של המחלקה שאליה הוספתם את ההערה הקודמת. חשוב לוודא שכוללים ערך ברירת מחדל עבור הסריאליזציה, שישמש אם עדיין לא נוצר קובץ.
object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings(exampleCounter = 0)
override suspend fun readFrom(input: InputStream): Settings =
try {
Json.decodeFromString<Settings>(
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(t)
.encodeToByteArray()
)
}
}
Proto DataStore
ההטמעה של Proto DataStore משתמשת ב-DataStore ובמאגרי אחסון לפרוטוקולים כדי לשמור אובייקטים מוקלדים בדיסק.
מאגר Proto DataStore דורש סכימה מוגדרת מראש בקובץ proto בספרייה app/src/main/proto/. הסכימה הזו מגדירה את הסוג של האובייקטים שאתם שומרים ב-Proto DataStore. מידע נוסף על הגדרת סכימת proto זמין במדריך השפה של protobuf.
מוסיפים קובץ בשם settings.proto בתוך התיקייה src/main/proto:
syntax = "proto3";
option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
מגדירים מחלקה שמטמיעה את Serializer<T>, כאשר T הוא הסוג שמוגדר בקובץ ה-proto. מחלקת הסריאליזציה הזו מגדירה איך DataStore קורא וכותב את סוג הנתונים שלכם. חשוב לוודא שכוללים ערך ברירת מחדל עבור הסריאליזציה, שישמש אם עדיין לא נוצר קובץ.
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) {
return t.writeTo(output)
}
}
יצירת DataStore
צריך לציין שם לקובץ שמשמש לשמירת הנתונים.
Preferences DataStore
ההטמעה של Preferences DataStore משתמשת במחלקות DataStore ו-Preferences כדי לשמור צמדי מפתח-ערך בדיסק. משתמשים בנציג המאפיין שנוצר על ידי preferencesDataStore כדי ליצור מופע של DataStore<Preferences>. קוראים לה פעם אחת ברמה העליונה של קובץ Kotlin. אפשר לגשת ל-DataStore דרך הנכס הזה בכל שאר חלקי האפליקציה. כך קל יותר לשמור על DataStore כ-singleton.
פרמטר name הוא חובה וזה השם של Preferences DataStore.
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
JSON DataStore
משתמשים בנציג המאפיין שנוצר על ידי dataStore כדי ליצור מופע של DataStore<T>, כאשר T הוא מחלקת הנתונים שניתנת לסריאליזציה. קוראים לה פעם אחת ברמה העליונה של קובץ Kotlin וניגשים אליה דרך נציג המאפיין הזה בכל שאר האפליקציה. הפרמטר fileName מציין ל-DataStore באיזה קובץ להשתמש כדי לאחסן את הנתונים, והפרמטר serializer מציין ל-DataStore את השם של מחלקת הסריאליזציה שהוגדרה קודם.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.json",
serializer = SettingsSerializer,
)
Proto DataStore
משתמשים בנציג המאפיין שנוצר על ידי dataStore כדי ליצור מופע של DataStore<T>, כאשר T הוא הסוג שמוגדר בקובץ ה-proto. קוראים לה פעם אחת ברמה העליונה של קובץ Kotlin וניגשים אליה דרך נציג המאפיין הזה בכל שאר האפליקציה. הפרמטר fileName מציין ל-DataStore באיזה קובץ להשתמש כדי לאחסן את הנתונים, והפרמטר serializer מציין ל-DataStore את השם של מחלקת הסריאליזציה שהוגדרה קודם.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer,
)
קריאה מ-DataStore
צריך לציין שם לקובץ שמשמש לשמירת הנתונים.
Preferences DataStore
מכיוון ש-Preferences DataStore לא משתמש בסכימה מוגדרת מראש, צריך להשתמש בפונקציה של סוג המפתח המתאים כדי להגדיר מפתח לכל ערך שרוצים לאחסן במופע DataStore<Preferences>. לדוגמה, כדי להגדיר מפתח לערך int, משתמשים ב-intPreferencesKey. לאחר מכן משתמשים במאפיין DataStore.data כדי לחשוף את הערך המתאים שמאוחסן באמצעות Flow.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
preferences[EXAMPLE_COUNTER] ?: 0
}
JSON DataStore
משתמשים ב-DataStore.data כדי לחשוף Flow של המאפיין המתאים מהאובייקט המאוחסן.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
settings.exampleCounter
}
Proto DataStore
משתמשים ב-DataStore.data כדי לחשוף Flow של המאפיין המתאים מהאובייקט המאוחסן.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
settings.exampleCounter
}
אפשר להשתמש ב-collectAsStateWithLifecycle כדי לצרוך את Flow שנוצר על ידי ViewModel בקומפוזיציה.
הפעולה הזו ממירה בבטחה את DataStore Flow למצב Compose שמפעיל קומפוזיציה מחדש.
@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
Text(text = "Example counter: ${counter}")
}
מידע נוסף על collectAsStateWithLifecycle זמין במאמר מצב ו-Jetpack פיתוח נייטיב.
כתיבה ל-DataStore
מאגר DataStore מספק פונקציה updateData שמעדכנת אובייקט מאוחסן באופן טרנזקציונלי. updateData מחזירה את המצב הנוכחי של הנתונים כמופע של סוג הנתונים, ומעדכנת את הנתונים באופן טרנזקציונלי בפעולת קריאה-כתיבה-שינוי אטומית. כל הקוד בבלוק updateData נחשב לטרנזקציה אחת.
Preferences DataStore
suspend fun incrementCounter() {
context.dataStore.updateData {
it.toMutablePreferences().also { preferences ->
preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
}
}
}
JSON DataStore
suspend fun incrementCounter() {
context.dataStore.updateData { settings ->
settings.copy(exampleCounter = settings.exampleCounter + 1)
}
}
Proto DataStore
suspend fun incrementCounter() {
context.dataStore.updateData { settings ->
settings.copy { exampleCounter = exampleCounter + 1 }
}
}
שימוש ב-DataStore באפליקציית Compose
כדי להשתמש ב-DataStore באפליקציית Compose, צריך לפעול לפי ההנחיות לארכיטקטורה של אפליקציות Android, כלומר לשמור את פעולות DataStore בשכבת הנתונים (למשל, במאגר) ולחשוף את הנתונים לממשק המשתמש באמצעות ViewModel.
מומלץ להימנע מקריאה מ-DataStore או מכתיבה ל-DataStore ישירות בתוך הפונקציות הניתנות להרכבה.
חשיפת DataStore דרך ViewModel מעבירים את המאגר (שעוטף את DataStore) אל
ViewModelוממירים אתFlowל-StateFlowכדי שממשק המשתמש יוכל לעקוב אחריו בקלות, כמו שמוצג בקטע הקוד הבא:class SettingsViewModel( private val userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { // Expose the DataStore flow as a StateFlow for Compose val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = UserSettings.getDefaultInstance() ) fun updateCounter(newValue: Int) { viewModelScope.launch { userPreferencesRepository.updateCounter(newValue) } } }התבוננות בכתיבה מהרכיב הניתן להרכבה. כדי לצפות ב-
StateFlowבממשק המשתמש בצורה בטוחה, משתמשים ב-collectAsStateWithLifecycleוקוראים לפונקציותViewModelכדי לטפל בפעולות כתיבה, כמו שמוצג בקטע הקוד הבא:@Composable fun SettingsScreen( viewModel: SettingsViewModel = viewModel() ) { // Safely collect the state val settings by viewModel.userSettings.collectAsStateWithLifecycle() Column(modifier = Modifier.padding(16.dp)) { Text(text = "Current counter: ${settings.counter}") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) { Text("Increment Counter") } } }
שימוש ב-DataStore בקוד מרובה תהליכים
אפשר להגדיר את DataStore כך שתהיה לו גישה לאותם נתונים בתהליכים שונים, עם אותן תכונות של עקביות נתונים כמו בתהליך יחיד. בפרט, DataStore מספק את המאפיינים הבאים:
- פעולות קריאה מחזירות רק את הנתונים שנשמרו בדיסק.
- עקביות של קריאה אחרי כתיבה.
- פעולות הכתיבה מבוצעות ברצף.
- קריאות אף פעם לא נחסמות על ידי כתיבות.
ניקח לדוגמה אפליקציה עם שירות ופעילות שבה השירות פועל בתהליך נפרד ומעדכן את Datastore באופן תקופתי.
בדוגמה הזו נעשה שימוש במאגר הנתונים JSON, אבל אפשר להשתמש גם במאגר נתונים Preferences או Proto.
@Serializable
data class Time(
val lastUpdateMillis: Long
)
ספריית סריאליזציה אומרת ל-DataStore איך לקרוא ולכתוב את סוג הנתונים. חשוב לוודא שכוללים ערך ברירת מחדל עבור הסריאליזציה, שישמש אם עדיין לא נוצר קובץ. הדוגמה הבאה היא הטמעה באמצעות kotlinx.serialization:
object TimeSerializer : Serializer<Time> {
override val defaultValue: Time = Time(lastUpdateMillis = 0L)
override suspend fun readFrom(input: InputStream): Time =
try {
Json.decodeFromString<Time>(
input.readBytes().decodeToString()
)
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read Time", serialization)
}
override suspend fun writeTo(t: Time, output: OutputStream) {
output.write(
Json.encodeToString(t)
.encodeToByteArray()
)
}
}
כדי להשתמש ב-DataStore בתהליכים שונים, צריך ליצור את אובייקט DataStore באמצעות MultiProcessDataStoreFactory גם עבור האפליקציה וגם עבור קוד השירות:
val dataStore = MultiProcessDataStoreFactory.create(
serializer = TimeSerializer,
produceFile = {
File("${context.filesDir.path}/time.pb")
},
corruptionHandler = null
)
מוסיפים את הנתונים הבאים לAndroidManifiest.xml:
<service
android:name=".TimestampUpdateService"
android:process=":my_process_id" />
השירות קורא מעת לעת ל-updateLastUpdateTime, שכותב למאגר הנתונים באמצעות updateData.
suspend fun updateLastUpdateTime() {
dataStore.updateData { time ->
time.copy(lastUpdateMillis = System.currentTimeMillis())
}
}
האפליקציה קוראת את הערך שנכתב על ידי השירות באמצעות זרימת הנתונים:
fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
time.lastUpdateMillis
}
עכשיו אפשר להכניס את כל הפונקציות האלה למחלקה שנקראת MultiProcessDataStore ולהשתמש בה באפליקציה.
זה קוד השירות:
class TimestampUpdateService : Service() {
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceScope.launch {
while (true) {
multiProcessDataStore.updateLastUpdateTime()
delay(1000)
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
}
וקוד האפליקציה:
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }
// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Last updated: $lastUpdateTime",
fontSize = 25.sp
)
DisposableEffect(context) {
val serviceIntent = Intent(context, TimestampUpdateService::class.java)
context.startService(serviceIntent)
onDispose {
context.stopService(serviceIntent)
}
}
אפשר להשתמש בהחדרת תלות של Hilt כדי שמופע DataStore יהיה ייחודי לכל תהליך:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
טיפול בקבצים פגומים
במקרים נדירים, קובץ ששמור בדיסק של DataStore עלול להינזק. כברירת מחדל, DataStore לא משחזר אוטומטית נתונים פגומים, וניסיונות לקרוא ממנו יגרמו למערכת להציג CorruptionException.
מאגר DataStore כולל API לטיפול בפגיעה בנתונים, שיכול לעזור לכם לשחזר את הנתונים בצורה חלקה במקרה כזה ולמנוע את השגיאה. אם מוגדרת תוכנית לטיפול בקבצים פגומים, היא מחליפה את הקובץ הפגום בקובץ חדש שמכיל ערך ברירת מחדל מוגדר מראש.
כדי להגדיר את ה-handler הזה, צריך לספק corruptionHandler כשיוצרים את מופע DataStore ב-by dataStore או בשיטת פקטוריDataStoreFactory:
val dataStore: DataStore<Settings> = DataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.filesDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
שליחת משוב
נשמח לקבל ממך משוב ורעיונות באמצעות המשאבים הבאים:
- Issue tracker:
- דיווח על בעיות כדי שנוכל לתקן באגים.
מקורות מידע נוספים
איפה אפשר למצוא מידע נוסף על Jetpack DataStore?
דוגמאות
בלוגים
Codelabs
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- טעינה והצגה של נתונים עם חלוקה לדפים
- סקירה כללית של LiveData
- פריסות וביטויי קשירה