DataStore جزء من Android Jetpack.

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

إذا كنت تستخدم حاليًا 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.0.0"

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

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

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

Kotlin

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

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

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

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

Proto DataStore

رائع

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

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

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

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

Kotlin

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

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

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

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

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

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

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

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

بما أنّ الإعدادات المفضّلة 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));

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

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 الملف الذي سيتم استخدامه لتخزين البيانات، وتخبر المعلمة 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")
   }
)

يتم إعلام 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(...)

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

يُرجى مشاركة ملاحظاتك وآرائك معنا من خلال الموارد التالية:

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

مراجع إضافية

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

عيّنات

المدوّنات

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