DataStore חלק מ-Android Jetpack.
Jetpack DataStore הוא פתרון לאחסון נתונים שמאפשר לכם לאחסן זוגות של מפתח/ערך או אובייקטים מוקלדים עם מאגרי פרוטוקולים. DataStore משתמש ב-coroutines וב-Flow של Kotlin כדי לאחסן נתונים באופן אסינכרוני, עקבי וטרנזקציונלי.
אם אתם משתמשים כרגע ב-SharedPreferences
לאחסון נתונים, כדאי לשקול מעבר ל-DataStore.
Preferences DataStore ו-Proto DataStore
DataStore מספק שתי הטמעות שונות: Preferences DataStore ו-Proto DataStore.
- Preferences DataStore מאחסן נתונים וניגש אליהם באמצעות מפתחות. ההטמעה הזו לא מחייבת סכימה מוגדרת מראש, והיא לא מספקת בטיחות סוגים.
- Proto DataStore מאחסן נתונים כמופעים של סוג נתונים מותאם אישית. ההטמעה הזו מחייבת הגדרה של סכימה באמצעות מאגרי פרוטוקולים, אבל היא מספקת בטיחות סוגים.
שימוש נכון ב-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, /*name=*/ "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
של הפונקציה מקבל בלוק קוד שבו אפשר לעדכן את הערכים לפי הצורך. כל הקוד בבלוק הטרנספורמציה נחשב לעסקה אחת.
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
מציין ל-DataStore את השם של מחלקת הסריאליזציה שהוגדרה בשלב 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();
קריאה מ-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();
ביצוע פעולות קלט/פלט סינכרוניות בשרשור של ממשק המשתמש עלול לגרום לשגיאות 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 איך לקרוא ולכתוב את סוג הנתונים.
חשוב לוודא שכוללים ערך ברירת מחדל עבור ה-serializer, שישמש אם עדיין לא נוצר קובץ. דוגמה להטמעה באמצעות 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
- פריסות וביטויי קשירה