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.1" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.1" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.1") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.1") }
Proto DataStore
Groovy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.1" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.1" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.1" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.1") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.1") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.1") }
אחסון צמדי מפתח/ערך באמצעות Preferences DataStore
ההטמעה של Preferences DataStore משתמשת ב-classes DataStore
ו-Preferences
כדי לשמור צמד מפתח/ערך פשוט בדיסק.
יצירת מאגר נתונים של העדפות
משתמשים במתווך הנכס שנוצר על ידי preferencesDataStore
כדי ליצור מופע של DataStore<Preferences>
. צריך להפעיל אותה פעם אחת ברמה העליונה של קובץ ה-Kotlin, ולגשת אליה דרך המאפיין הזה בשאר האפליקציה. כך קל יותר לשמור על DataStore
כ-singleton. לחלופין, אם אתם משתמשים ב-RxJava, תוכלו להשתמש ב-RxPreferenceDataStoreBuilder
. הפרמטר 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 language.
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
יצירת Proto DataStore
יש שני שלבים ליצירת Proto DataStore לאחסון אובייקטים מסוגים שונים:
- מגדירים כיתה שמטמיעה את
Serializer<T>
, כאשרT
הוא הסוג שמוגדר בקובץ ה-proto. סוג הסריאליזציה הזה מורה ל-DataStore איך לקרוא ולכתוב את סוג הנתונים. חשוב לכלול ערך ברירת מחדל לסורק כדי שייעשה בו שימוש אם עדיין לא נוצר קובץ. - משתמשים במתווך הנכס שנוצר על ידי
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): Timer =
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
- פריסות וביטויי קישור