‫DataStore   جزء من Android Jetpack.

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

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

تفضيلات DataStore وProto DataStore

يوفّر DataStore طريقتَي تنفيذ مختلفتَين: Preferences DataStore و Proto DataStore.

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

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

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

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

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

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

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

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

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

رائع

    // 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

رائع

    // 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 فئتَي DataStore و Preferences للحفاظ على أزواج المفتاح/القيمة البسيطة على القرص.

إنشاء ملف Preferences DataStore

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

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(...)

التعامل مع تلف الملفات

في حالات نادرة، قد يتم تلف ملف DataStore الثابت على القرص. لا يتم تلقائيًا استرداد DataStore من التلف، ومحاولة القراءة منه ستؤدي إلى طرح النظام لخطأ CorruptionException.

يوفّر DataStore واجهة برمجة تطبيقات لمعالجة التلف يمكنها مساعدتك في الاسترداد بنجاح في مثل هذا السيناريو، وتجنُّب طرح الاستثناء. عند ضبطه، يستبدل معالج الفساد الملفّ الذي يتضمّن فسادًا بملفّ جديد يحتوي على قيمة تلقائية محدّدة مسبقًا.

لإعداد هذا المعالج، وفّر 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، اطّلِع على المراجع الإضافية التالية:

نماذج

المدوّنات

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