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

GroovyKotlin
    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.1.5"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.5"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.5"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.5"
    }
    
    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.5")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.5")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.5")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.5")
    }
    

Proto DataStore

GroovyKotlin
    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.5"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.5"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.5"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.5"
    }
    
    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.5")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.5")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.5")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.5")
    }
    

אחסון צמדי מפתח/ערך באמצעות Preferences DataStore

ההטמעה של Preferences DataStore משתמשת ב-classes‏ DataStore ו-Preferences כדי לשמור צמד מפתח/ערך פשוט בדיסק.

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

משתמשים במתווך הנכס שנוצר על ידי preferencesDataStore כדי ליצור מופע של DataStore<Preferences>. צריך להפעיל אותה פעם אחת ברמה העליונה של קובץ ה-Kotlin, ולגשת אליה דרך המאפיין הזה בשאר האפליקציה. כך קל יותר לשמור על DataStore כ-singleton. לחלופין, אפשר להשתמש ב-RxPreferenceDataStoreBuilder אם משתמשים ב-RxJava. הפרמטר name הוא חובה, והוא השם של מאגר הנתונים של ההעדפות.

KotlinJava
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

קריאה מ-Preferences DataStore

מכיוון שב-Preferences DataStore לא נעשה שימוש בסכימה מוגדרת מראש, צריך להשתמש בפונקציה המתאימה של סוג המפתח כדי להגדיר מפתח לכל ערך שרוצים לאחסן במכונה DataStore<Preferences>. לדוגמה, כדי להגדיר מפתח לערך int, משתמשים ב-intPreferencesKey(). לאחר מכן, משתמשים במאפיין DataStore.data כדי לחשוף את הערך המאוחסן המתאים באמצעות Flow.

KotlinJava
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}
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 של הפונקציה מקבל בלוק קוד שבו אפשר לעדכן את הערכים לפי הצורך. כל הקוד בבלוק הטרנספורמציה מטופל כעסקה אחת.

KotlinJava
suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}
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 לאחסון אובייקטים מוגדרים מסוג:

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

KotlinJava
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }
Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

כתיבת ב-Proto DataStore

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

KotlinJava
suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}
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 מחזיר נתונים:

KotlinJava
val exampleData = runBlocking { context.dataStore.data.first() }
Settings settings = dataStore.data().blockingFirst();

ביצוע פעולות קלט/פלט (I/O) סינכרוניות בשרשור של ממשק המשתמש עלול לגרום לשגיאות ANR או לתנודות ב-UI. כדי לצמצם את הבעיות האלה, אפשר לטעון מראש את הנתונים מ-DataStore באופן אסינכררוני:

KotlinJava
override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}
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): 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 זמין במקורות המידע הבאים:

דוגמיות

Learn how this app was designed and built in the design case study, architecture learning journey and modularization learning journey. This is the repository for the Now in Android app. It is a work in progress 🚧. Now in Android is a fully functional

בלוגים

Codelabs

Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.

עדכון אחרון: Dec 22, 2024

שימוש ב-LiveData כדי לטפל בנתונים בהתאם למחזור החיים.

עדכון אחרון: Dec 22, 2024

Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.

עדכון אחרון: Sep 20, 2024