DataStore جزء من Android Jetpack.

إنّ Jetpack DataStore هو حلّ لتخزين البيانات يسمح لك بتخزين أزواج المفتاح/القيمة أو الكائنات المكتوبة باستخدام مخازن مؤقتة للبروتوكول. يستخدم DataStore كورروتينات Kotlin وFlow لتخزين البيانات بشكل غير متزامن ومتسق وبطريقة معاملات.

إذا كنت تستخدم حاليًا SharedPreferences لتخزين البيانات، يمكنك نقل البيانات إلى DataStore بدلاً من ذلك.

الإعدادات المفضّلة لكل من DataStore وProto DataStore

يوفر DataStore عمليتين مختلفتين: Preferences DataStore وProto DataStore.

  • يخزِّن الإعدادات المفضّلة DataStore البيانات ويصل إليها باستخدام المفاتيح. لا يتطلب هذا التنفيذ مخططًا محددًا مسبقًا، ولا يوفر أمان النوع.
  • يخزِّن Proto DataStore البيانات كمثيلات لنوع بيانات مخصّص. ويتطلب هذا التنفيذ تحديد مخطط باستخدام المخازن المؤقتة للبروتوكولات، ولكنه يوفّر أمان النوع.

استخدام DataStore بشكل صحيح

لاستخدام DataStore بشكل صحيح، ضع دائمًا في اعتبارك القواعد التالية:

  1. يجب عدم إنشاء أكثر من مثيل واحد من DataStore لملف معيّن في العملية نفسها. ويمكن أن يؤدي ذلك إلى إيقاف جميع وظائف DataStore. إذا كان هناك العديد من "مخازن البيانات" النشطة لملف معيّن في العملية نفسها، سيطرح DataStore رمز IllegalStateException عند قراءة البيانات أو تعديلها.

  2. يجب أن يكون النوع العام من DataStore غير قابل للتغيير. يؤدي تغيير نوع مستخدم في DataStore إلى إبطال أي ضمانات يوفّرها DataStore وإنشاء أخطاء يُحتمَل أن تكون خطيرة ويصعب رصدها. يوصى بشدة باستخدام مخازن البروتوكول المؤقتة التي توفر ضمانات عدم قابلية التغيير، وواجهة برمجة تطبيقات بسيطة، وتسلسلاً فعالاً.

  3. لا تخلط بين استخدامات SingleProcessDataStore وMultiProcessDataStore للملف نفسه. إذا كنت تريد الوصول إلى DataStore من أكثر من عملية، استخدِم دائمًا MultiProcessDataStore.

ضبط إعدادات الجهاز

لاستخدام Jetpack DataStore في تطبيقك، أضِف ما يلي إلى ملف Gradle بناءً على عملية التنفيذ التي تريد استخدامها:

الإعدادات المفضّلة DataStore

رائع

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

رائع

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

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

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

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

Kotlin

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

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

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

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

تخزين أزواج المفتاح/القيمة باستخدام صفحة "الإعدادات المفضّلة" لتخزين البيانات

إنّ تنفيذ الإعدادات المفضّلة في DataStore يستخدم فئتَي DataStore وPreferences للاحتفاظ بأزواج المفتاح/القيمة البسيطة إلى القرص.

إنشاء واجهة برمجة تطبيقات مفضّلة

استخدِم مفوَّض الموقع الذي تم إنشاؤه من قِبل preferencesDataStore لإنشاء مثيل Datastore<Preferences>. استدعِ هذا الرمز مرة واحدة في المستوى الأعلى من ملف kotlin، ثم ادخل إليه من خلال هذه السمة في باقي التطبيق. يسهّل ذلك الاحتفاظ بـ "DataStore" كوحدة واحدة. بدلاً من ذلك، يمكنك استخدام 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();

القراءة من أحد متاجر البيانات المفضّلة

بما أنّ الإعدادات المفضّلة في DataStore لا تستخدم مخططًا محدّدًا مسبقًا، عليك استخدام دالة نوع المفتاح المقابل لتحديد مفتاح لكل قيمة تحتاج إلى تخزينها في مثيل DataStore<Preferences>. على سبيل المثال، لتحديد مفتاح لقيمة عدد صحيح، استخدِم 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));

الكتابة في صفحة "مخزن البيانات المفضَّلة"

يوفّر 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. لمعرفة المزيد حول تعريف مخطط النموذج الأوّلي، يمكنك مراجعة دليل لغة النموذج الأوّلي.

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 باسم فئة المسلسل التسلسلي المحددة في الخطوة 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 في واجهة برمجة التطبيقات غير المتزامنة، ولكن قد لا يكون من الممكن دائمًا تغيير الرمز المحيط بك حتى يكون غير متزامن. قد يحدث ذلك إذا كنت تعمل على قاعدة رموز برمجية تستخدم وحدات الإدخال والإخراج المتزامنة للقرص، أو إذا كانت لديك تبعية لا توفّر واجهة برمجة تطبيقات غير متزامنة.

توفِّر الكوروتينات في لغة 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 ما يلي:

  • لا تعرض ميزة القراءة سوى البيانات التي تم الاحتفاظ بها على القرص.
  • الاتّساق في القراءة بعد الكتابة
  • تكون الكتابات متسلسلة.
  • لا يتم حظر القرّاء مطلقًا من خلال عمليات الكتابة.

جرِّب نموذج تطبيق يتضمّن خدمة ونشاطًا:

  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 بكيفية قراءة نوع البيانات وكتابته. تأكد من تضمين قيمة افتراضية للتسلسل لاستخدامه إذا لم يتم إنشاء ملف بعد. في ما يلي مثال على عملية التنفيذ باستخدام 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(...)

تقديم الملاحظات

يُرجى مشاركة ملاحظاتك وأفكارك معنا من خلال المراجع التالية:

أداة تتبّع المشاكل
أبلِغ عن المشاكل لنتمكّن من إصلاح الأخطاء.

مصادر إضافية

لمزيد من المعلومات حول Jetpack DataStore، اطّلِع على المراجع الإضافية التالية:

العيّنات

المدوّنات

الدروس التطبيقية حول الترميز