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.
Preferences DataStore ו-Proto DataStore
DataStore מספק שתי הטמעות שונות: Preferences DataStore ו-Proto DataStore.
- Preferences DataStore מאחסן נתונים וניגש אליהם באמצעות מפתחות. ההטמעה הזו לא מחייבת סכימה מוגדרת מראש, והיא לא מספקת בטיחות סוגים.
- Proto DataStore מאחסן נתונים כמופעים של סוג נתונים מותאם אישית. ההטמעה הזו מחייבת הגדרה של סכימה באמצעות protocol buffers, אבל היא מספקת בטיחות סוגים.
שימוש נכון ב-DataStore
כדי להשתמש ב-DataStore בצורה נכונה, חשוב לזכור תמיד את הכללים הבאים:
לעולם אל תיצרו יותר ממופע אחד של
DataStore
עבור קובץ נתון באותו תהליך. פעולה כזו עלולה לשבור את כל הפונקציונליות של DataStore. אם יש כמה DataStore פעילים לקובץ נתון באותו תהליך, DataStore יחזירIllegalStateException
כשקוראים או מעדכנים נתונים.הסוג הגנרי של
DataStore<T>
חייב להיות בלתי ניתן לשינוי. שינוי של סוג שמשמש ב-DataStore מבטל את העקביות ש-DataStore מספק, ויוצר באגים שעלולים להיות חמורים וקשים לאיתור. מומלץ להשתמש ב-protocol buffers, שעוזרים להבטיח אי-שינוי, API ברור וסדרות יעילות.אל תערבבו בין שימושים במאפיינים
SingleProcessDataStore
ו-MultiProcessDataStore
באותו קובץ. אם אתם מתכוונים לגשת אלDataStore
מיותר מתהליך אחד, אתם צריכים להשתמש ב-MultiProcessDataStore
.
הגדרה
כדי להשתמש ב-Jetpack DataStore באפליקציה, מוסיפים את השורות הבאות לקובץ Gradle, בהתאם להטמעה שרוצים להשתמש בה:
Preferences DataStore
Groovy
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
Proto DataStore
Groovy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.7" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.7") }
אחסון של צמדים של מפתח/ערך באמצעות Preferences DataStore
ההטמעה של Preferences DataStore משתמשת במחלקות DataStore
ו-Preferences
כדי לשמור צמדי מפתח-ערך בדיסק.
יצירה של מאגר נתונים להעדפות
משתמשים בנציג הנכס שנוצר על ידי preferencesDataStore
כדי ליצור מופע של DataStore<Preferences>
. קוראים לה פעם אחת ברמה העליונה של קובץ ה-Kotlin, וניגשים אליה דרך המאפיין הזה בכל שאר האפליקציה. כך קל יותר לשמור על DataStore
כ-singleton.
אפשר גם להשתמש ב-RxPreferenceDataStoreBuilder
אם אתם משתמשים ב-RxJava.
הפרמטר name
הוא חובה והוא השם של מאגר נתוני ההעדפות.
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, "settings").build();
קריאה מ-Preferences DataStore
מכיוון ש-Preferences DataStore לא משתמש בסכימה מוגדרת מראש, צריך להשתמש בפונקציה המתאימה של סוג המפתח כדי להגדיר מפתח לכל ערך שצריך לאחסן במופע DataStore<Preferences>
. לדוגמה, כדי להגדיר מפתח לערך int, משתמשים ב-intPreferencesKey()
. לאחר מכן משתמשים במאפיין DataStore.data
כדי לחשוף את הערך המתאים ששמור באמצעות 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));
כתיבה ל-Preferences DataStore
Preferences DataStore מספק פונקציה edit()
שמעדכנת את הנתונים ב-DataStore
באופן טרנזקציונלי. הפרמטר transform
של הפונקציה מקבל בלוק קוד שבו אפשר לעדכן את הערכים לפי הצורך. כל הקוד בבלוק transform נחשב לעסקה אחת.
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.
אחסון אובייקטים מוקלדים באמצעות Proto DataStore
ההטמעה של Proto DataStore משתמשת ב-DataStore ובמאגרי פרוטוקולים כדי לשמור אובייקטים מוקלדים בדיסק.
הגדרת סכימה
Proto DataStore דורש סכימה מוגדרת מראש בקובץ proto בספרייה app/src/main/proto/
. הסכימה הזו מגדירה את הסוג של האובייקטים שאתם שומרים ב-Proto DataStore. מידע נוסף על הגדרת סכמת פרוטו זמין במדריך השפה של protobuf.
syntax = "proto3";
option java_package = "com.example.application.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
יצירת Proto DataStore
תהליך היצירה של Proto DataStore לאחסון אובייקטים מוקלדים כולל שני שלבים:
- מגדירים מחלקה שמטמיעה את
Serializer<T>
, כאשרT
הוא הסוג שמוגדר בקובץ הפרוטו. מחלקת הסריאליזציה הזו אומרת ל-DataStore איך לקרוא ולכתוב את סוג הנתונים. חשוב לוודא שכוללים ערך ברירת מחדל עבור ה-serializer, שישמש אם עדיין לא נוצר קובץ. - משתמשים בנציג המאפיין שנוצר על ידי
dataStore
כדי ליצור מופע שלDataStore<T>
, כאשרT
הוא הסוג שמוגדר בקובץ הפרוטו. צריך להפעיל את הפונקציה הזו פעם אחת ברמה העליונה של קובץ ה-Kotlin, ולגשת אליה דרך נציג המאפיין הזה בכל שאר האפליקציה. הפרמטרfilename
מציין ל-DataStore באיזה קובץ להשתמש כדי לאחסן את הנתונים, והפרמטרserializer
מציין את השם של מחלקת הסריאליזציה שהוגדרה בשלב 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() {
return Settings.getDefaultInstance();
}
@Override
public Settings readFrom(@NotNull InputStream input) {
try {
return Settings.parseFrom(input);
} catch (InvalidProtocolBufferException exception) {
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();
קריאה מ-Proto DataStore
משתמשים ב-DataStore.data
כדי לחשוף Flow
של המאפיין המתאים מהאובייקט המאוחסן.
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());
כתיבה ל-Proto DataStore
Proto DataStore מספק פונקציה updateData()
שמעדכנת אובייקט מאוחסן באופן טרנזקציונלי. updateData()
מחזירה את המצב הנוכחי של הנתונים כמופע של סוג הנתונים, ומעדכנת את הנתונים באופן טרנזקציונלי בפעולת קריאה-כתיבה-שינוי אטומית.
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()));
שימוש ב-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 מספק:
- פעולות קריאה מחזירות רק את הנתונים שנשמרו בדיסק.
- עקביות של קריאה אחרי כתיבה.
- פעולות הכתיבה מבוצעות ברצף.
- קריאות אף פעם לא נחסמות על ידי כתיבות.
נניח שיש אפליקציה לדוגמה עם שירות ופעילות:
השירות פועל בתהליך נפרד ומעדכן מעת לעת את 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) } } }
האפליקציה תאסוף את השינויים האלה ותעדכן את ממשק המשתמש שלה
val settings: Settings by dataStore.data.collectAsState() Text( text = "Last updated: $${settings.timestamp}", )
כדי להשתמש ב-DataStore בתהליכים שונים, צריך ליצור את אובייקט DataStore באמצעות MultiProcessDataStoreFactory
.
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
serializer
אומר ל-DataStore איך לקרוא ולכתוב את סוג הנתונים. חשוב לוודא שכוללים ערך ברירת מחדל עבור הסריאליזציה, שישמש אם עדיין לא נוצר קובץ. הדוגמה הבאה היא הטמעה באמצעות 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): Settings =
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()
)
}
}
אפשר להשתמש בהזרקת תלות של 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
- פריסות וביטויי קישור