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 בצורה נכונה, חשוב תמיד לזכור את הכללים הבאים:

  1. אף פעם אל תיצרו יותר ממופעים אחדים של DataStore לקובץ נתון באותו תהליך. הפעולה הזו עלולה לשבש את כל הפונקציונליות של DataStore. אם יש כמה מאגרי DataStore פעילים לקובץ נתון באותו תהליך, המערכת תזרוק את השגיאה IllegalStateException בזמן קריאה או עדכון של הנתונים.

  2. הסוג הכללי של DataStore חייב להיות בלתי ניתן לשינוי. שינוי של סוג שמשמש ב-DataStore מבטל את כל ההתחייבויות של DataStore ויוצר באגים שעשויים להיות חמורים וקשים לזיהוי. מומלץ מאוד להשתמש במאגרי פרוטוקולים שמספקים ערבויות לבלתי לשינוי, ממשק API פשוט וסריאליזציה יעילה.

  3. אף פעם אל תערבבו בין 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 משתמשת ב-class‏ DataStore וב-class‏ Preferences כדי לשמור צמד מפתח/ערך פשוט בדיסק.

יצירת מאגר נתונים של העדפות

משתמשים במתווך הנכס שנוצר על ידי preferencesDataStore כדי ליצור מופע של DataStore<Preferences>. צריך להפעיל אותה פעם אחת ברמה העליונה של קובץ ה-Kotlin, ולגשת אליה דרך המאפיין הזה בשאר האפליקציה. כך יהיה קל יותר לשמור על ה-DataStore כיחידה אחת. לחלופין, אם אתם משתמשים ב-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 לאחסון אובייקטים מוגדרים מסוג:

  1. מגדירים כיתה שמטמיעה את Serializer<T>, כאשר T הוא הסוג שמוגדר בקובץ ה-proto. מחלקה של בהמשכים טורית מנחה את DataStore איך לקרוא ולכתוב את סוג הנתונים. חשוב לכלול ערך ברירת מחדל לסורק כדי שייעשה בו שימוש אם עדיין לא נוצר קובץ.
  2. משתמשים במתווך הנכס שנוצר על ידי 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 מבטיח:

  • קריאות מחזירות רק את הנתונים ששמורים בדיסק.
  • עקביות של קריאה אחרי כתיבה.
  • הכתיבה מתבצעת בסדרה.
  • פעולות הכתיבה אף פעם לא נחסמות.

נבחן אפליקציה לדוגמה עם שירות ופעילות:

  1. השירות פועל בתהליך נפרד ומעדכן מדי פעם את 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)
              }
          }
    }
    
  2. האפליקציה תאסוף את השינויים האלה ותעדכן את ממשק המשתמש שלה.

    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 לטיפול בפגיעה שיכול לעזור לכם להתאושש בצורה חלקה בתרחיש כזה, ולהימנע מהשלכת החריגה. כשמגדירים אותו, מנהל השגיאות מחליף את הקובץ הפגום בקובץ חדש שמכיל ערך ברירת מחדל מוגדר מראש.

כדי להגדיר את ה-handler הזה, צריך לציין corruptionHandler כשיוצרים את המכונה של DataStore ב-by dataStore() או ב-method 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