Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: Explore this app architecture guide on data layer libraries to learn about Preferences DataStore and Proto DataStore, Setup, and more. hide_page_heading: true
DataStore חלק מ-Android Jetpack.
Jetpack DataStore הוא פתרון לאחסון נתונים שמאפשר לכם לאחסן זוגות של מפתח/ערך או אובייקטים מוקלדים באמצעות מאגרי פרוטוקולים. DataStore משתמש ב-Kotlin coroutines וב-Flow כדי לאחסן נתונים באופן אסינכרוני, עקבי וטרנזקציונלי.
אם אתם משתמשים ב-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 כדי להמיר אותם לפורמט שניתן לשמירה. אתם יכולים לבחור להשתמש ב-Protocol Buffers, ב-JSON או בכל שיטת סריאליזציה אחרת.
הגדרה
כדי להשתמש ב-Jetpack DataStore באפליקציה, מוסיפים את השורות הבאות לקובץ Gradle, בהתאם להטמעה שרוצים להשתמש בה:
Preferences DataStore
מוסיפים את השורות הבאות לחלק של יחסי התלות בקובץ gradle:
Groovy
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation "androidx.datastore:datastore-preferences:1.1.7" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Kotlin
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation("androidx.datastore:datastore-preferences:1.1.7") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
כדי להוסיף תמיכה אופציונלית ב-RxJava, מוסיפים את יחסי התלות הבאים:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") }
DataStore
מוסיפים את השורות הבאות לחלק של יחסי התלות בקובץ gradle:
Groovy
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation "androidx.datastore:datastore:1.1.7" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-core:1.1.7" }
Kotlin
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation("androidx.datastore:datastore:1.1.7") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-core:1.1.7") }
מוסיפים את יחסי התלות האופציונליים הבאים לתמיכה ב-RxJava:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") }
כדי לבצע סריאליזציה של תוכן, מוסיפים תלות בסריאליזציה של Protocol Buffers או JSON.
JSON serialization
כדי להשתמש בסריאליזציה של 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 פעילים לקובץ נתון באותו תהליך, DataStore יחזירIllegalStateExceptionכשקוראים או מעדכנים נתונים.הסוג הגנרי של
DataStore<T>חייב להיות בלתי ניתן לשינוי. שינוי של סוג שמשמש ב-DataStore מבטל את העקביות ש-DataStore מספק, ויוצר באגים שעלולים להיות חמורים וקשים לאיתור. מומלץ להשתמש ב-protocol buffers, שעוזרים להבטיח אי-שינוי, 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. מידע נוסף על הגדרת סכמת פרוטו זמין במדריך השפה של 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 הוא הסוג שמוגדר בקובץ הפרוטו. מחלקת הסריאליזציה הזו מגדירה איך DataStore קורא וכותב את סוג הנתונים שלכם. חשוב לוודא שכוללים ערך ברירת מחדל עבור ה-serializer, שישמש אם עדיין לא נוצר קובץ.
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)
}
}
יצירת מאגר נתונים
צריך לציין שם לקובץ שמשמש לשמירת הנתונים.
Preferences DataStore
ההטמעה של Preferences DataStore משתמשת במחלקות DataStore ו-Preferences כדי לשמור צמדי מפתח-ערך בדיסק. משתמשים בנציג המאפיין שנוצר על ידי preferencesDataStore כדי ליצור מופע של DataStore<Preferences>. קוראים לה פעם אחת ברמה העליונה של קובץ Kotlin. גישה ל-DataStore דרך הנכס הזה בכל שאר חלקי האפליקציה. כך קל יותר לשמור על DataStore כ-singleton.
אפשרות אחרת היא להשתמש ב-RxPreferenceDataStoreBuilder אם אתם משתמשים ב-RxJava.
הפרמטר name הוא חובה והוא השם של מאגר נתוני ההעדפות.
// 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 את השם של מחלקת הסריאליזציה שהוגדרה בשלב 1.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.json",
serializer = SettingsSerializer,
)
Proto DataStore
משתמשים בנציג המאפיין שנוצר על ידי dataStore כדי ליצור מופע של DataStore<T>, כאשר T הוא הסוג שמוגדר בקובץ הפרוטו. קוראים לה פעם אחת ברמה העליונה של קובץ Kotlin וניגשים אליה דרך נציג המאפיין הזה בכל שאר האפליקציה. הפרמטר fileName מציין ל-DataStore באיזה קובץ להשתמש כדי לאחסן את הנתונים, והפרמטר serializer מציין ל-DataStore את השם של מחלקת הסריאליזציה שהוגדרה בשלב 1.
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
}
כתיבה ל-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 }
}
}
דוגמה לכתיבה
אפשר לשלב את הפונקציות האלה בכיתה ולהשתמש בהן באפליקציית Compose.
Preferences DataStore
עכשיו אפשר להכניס את הפונקציות האלה למחלקה שנקראת PreferencesDataStore ולהשתמש בה באפליקציית Compose.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }
// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(
onClick = {
coroutineScope.launch { preferencesDataStore.incrementCounter() }
}
) {
Text("increment")
}
JSON DataStore
עכשיו אפשר להכניס את הפונקציות האלה למחלקה שנקראת JSONDataStore ולהשתמש בה באפליקציית Compose.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }
// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
Text("increment")
}
Proto DataStore
עכשיו אפשר להכניס את הפונקציות האלה למחלקה שנקראת ProtoDataStore ולהשתמש בה באפליקציית Compose.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }
// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
Text("increment")
}
שימוש ב-DataStore בקוד סינכרוני
אחד היתרונות העיקריים של DataStore הוא ה-API האסינכרוני, אבל לא תמיד אפשר לשנות את הקוד שמסביב כך שיהיה אסינכרוני. זה יכול לקרות אם אתם עובדים עם בסיס קוד קיים שמשתמש בקלט/פלט סינכרוני בדיסק, או אם יש לכם תלות שלא מספקת API אסינכרוני.
קורוטינות של Kotlin מספקות את כלי הקורוטינות runBlocking() כדי לגשר על הפער בין קוד סינכרוני לקוד אסינכרוני. אפשר להשתמש ב-runBlocking() כדי לקרוא נתונים מ-DataStore באופן סינכרוני. RxJava מציעה שיטות חסימה ב-Flowable. בלוק הקוד הבא חוסם את השרשור שקורא עד ש-DataStore מחזיר נתונים:
Kotlin
val exampleData = runBlocking { context.dataStore.data.first() }
Java
Settings settings = dataStore.data().blockingFirst();
ביצוע פעולות קלט/פלט סינכרוניות בשרשור ה-UI עלול לגרום לשגיאות ANR או לממשק משתמש לא מגיב. כדי לצמצם את הבעיות האלה, אפשר לטעון מראש את הנתונים מ-DataStore באופן אסינכרוני:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
context.dataStore.data.first()
// You should also handle IOExceptions here.
}
}
Java
dataStore.data().first().subscribe();
בדרך הזו, DataStore קורא את הנתונים באופן אסינכרוני ומאחסן אותם במטמון בזיכרון. קריאות סינכרוניות מאוחרות יותר באמצעות runBlocking() עשויות להיות מהירות יותר או להימנע לחלוטין מפעולת קלט/פלט בדיסק אם הקריאה הראשונית הושלמה.
שימוש ב-DataStore בקוד מרובה תהליכים
אתם יכולים להגדיר את DataStore כך שתהיה לו גישה לאותם נתונים בתהליכים שונים, עם אותן תכונות של עקביות נתונים כמו בתהליך יחיד. בפרט, DataStore מספק:
- פעולות קריאה מחזירות רק את הנתונים שנשמרו בדיסק.
- עקביות של קריאה אחרי כתיבה.
- פעולות הכתיבה מבוצעות ברצף.
- קריאות אף פעם לא נחסמות על ידי כתיבות.
ניקח לדוגמה אפליקציה עם שירות ופעילות שבה השירות פועל בתהליך נפרד ומעדכן את מאגר הנתונים באופן תקופתי.
בדוגמה הזו נעשה שימוש בחנות נתונים של JSON, אבל אפשר להשתמש גם בחנות נתונים של העדפות או של פרוטו.
@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.cacheDir.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() או בשיטת factory DataStoreFactory:
val dataStore: DataStore<Settings> = DataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
שליחת משוב
נשמח לקבל ממך משוב ורעיונות באמצעות מקורות המידע הבאים:
- מעקב אחר בעיות:
- דיווח על בעיות כדי שנוכל לתקן באגים.
מקורות מידע נוספים
מידע נוסף על Jetpack DataStore זמין במקורות המידע הבאים:
טעימות
בלוגים
Codelabs
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- טעינה והצגה של נתונים עם חלוקה לדפים
- סקירה כללית של LiveData
- פריסות וביטויי קשירה