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 פעילים לקובץ נתון באותו תהליך, המערכת תשליך את השגיאהIllegalStateException
בזמן קריאת הנתונים או עדכון שלהם.הסוג הכללי של DataStore
חייב להיות בלתי ניתן לשינוי. שינוי של סוג שמשמש ב-DataStore מבטל את כל ההתחייבויות של DataStore ויוצר באגים שעשויים להיות חמורים וקשים לזיהוי. מומלץ מאוד להשתמש במאגרי פרוטוקולים שמספקים ערבויות לבלתי-שינוי, ממשק API פשוט וסריאליזציה יעילה.אף פעם אל תערבבו בין
SingleProcessDataStore
לביןMultiProcessDataStore
באותו קובץ. אם אתם מתכוונים לגשת ל-DataStore
מכמה תהליכים, תמיד צריך להשתמש ב-MultiProcessDataStore
.
הגדרה
כדי להשתמש ב-Jetpack DataStore באפליקציה, מוסיפים את הקוד הבא לקובץ Gradle, בהתאם להטמעה שבה רוצים להשתמש:
Preferences DataStore
Groovy
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.2" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.2" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.2" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.2" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.2") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.2") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.2") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.2") }
Proto DataStore
Groovy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.2" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.2" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.2" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.2" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.2") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.2") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.2") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.2") }
אחסון צמדים של מפתח/ערך באמצעות Preferences DataStore
ההטמעה של Preferences DataStore משתמשת ב-class DataStore
וב-class Preferences
כדי לשמור צמד מפתח/ערך פשוט בדיסק.
יצירת מאגר נתונים של העדפות
משתמשים במתווך הנכס שנוצר על ידי preferencesDataStore
כדי ליצור מופע של DataStore<Preferences>
. צריך להפעיל אותה פעם אחת ברמה העליונה של קובץ ה-Kotlin, ולגשת אליה דרך המאפיין הזה בשאר האפליקציה. כך קל יותר לשמור על DataStore
כ-singleton. לחלופין, אפשר להשתמש ב-RxPreferenceDataStoreBuilder
אם משתמשים ב-RxJava. הפרמטר name
הוא חובה, והוא השם של Preferences DataStore.
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. מידע נוסף על הגדרת סכימה של proto זמין במדריך לשפת 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
הוא הסוג שמוגדר בקובץ ה-proto. סוג הסריאליזציה הזה מורה ל-DataStore איך לקרוא ולכתוב את סוג הנתונים. חשוב לכלול ערך ברירת מחדל ל-serializer, שייעשה בו שימוש אם עדיין לא נוצר קובץ. - משתמשים במתווך הנכס שנוצר על ידי
dataStore
כדי ליצור מכונה שלDataStore<T>
, כאשרT
הוא הסוג שמוגדר בקובץ ה-proto. צריך להפעיל את הפונקציה הזו פעם אחת ברמה העליונה של קובץ ה-Kotlin, ולגשת אליה דרך נציג הנכס הזה בשאר האפליקציה. הפרמטרfilename
אומר ל-DataStore באיזה קובץ יש להשתמש כדי לאחסן את הנתונים, והפרמטרserializer
אומר ל-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() { 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 האסינכרוני, אבל לא תמיד אפשר לשנות את הקוד שמקיף אותו כך שיהיה אסינכרוני. יכול להיות מצב כזה אם אתם עובדים עם קוד בסיס קיים שמשתמש ב-I/O מסונכרן בדיסק, או אם יש לכם תלות שלא מספקת API אסינכררוני.
ב-Kotlin, פונקציות קורוטין מספקות את ה-builder של פונקציות הקורוטין runBlocking()
, שעוזר לגשר על הפער בין קוד סינכרוני לקוד אסינכרוני. אפשר להשתמש ב-runBlocking()
כדי לקרוא נתונים מ-Datastore באופן סינכרוני.
ב-RxJava יש שיטות חסימה ב-Flowable
. הקוד הבא חוסם את שרשור הקריאה עד ש-DataStore מחזיר נתונים:
Kotlin
val exampleData = runBlocking { context.dataStore.data.first() }
Java
Settings settings = dataStore.data().blockingFirst();
ביצוע פעולות קלט/פלט (I/O) סינכרוניות בשרשור של ממשק המשתמש עלול לגרום לשגיאות ANR או לתנודות ב-UI. כדי לצמצם את הבעיות האלה, אפשר לטעון מראש את הנתונים מ-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 לטיפול בפגיעה שיכול לעזור לכם להתאושש בצורה חלקה בתרחיש כזה, ולהימנע מהשלכת החריגה. כשמגדירים אותו, מנהל השגיאות מחליף את הקובץ הפגום בקובץ חדש שמכיל ערך ברירת מחדל מוגדר מראש.
כדי להגדיר את הטיפול הזה, צריך לספק corruptionHandler
כשיוצרים את מופע DataStore ב-by dataStore()
או בשיטת המפעל 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
- פריסות וביטויי קישור