بنية التطبيق: طبقة البيانات - DataStore - مطوّرو برامج Android

Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: Explore this app architecture guide on data layer libraries to learn about Preferences DataStore and Proto DataStore, Setup, and more. hide_page_heading: true

DataStore   أحد مكونات Android Jetpack.

تجربة Kotlin Multiplatform
تتيح Kotlin Multiplatform مشاركة طبقة البيانات مع منصات أخرى. كيفية إعداد DataStore واستخدامه في KMP

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

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

‫Preferences DataStore وProto DataStore

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

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

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

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

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

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

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

ضبط إعدادات الميزة

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

متجر البيانات في الإعدادات المفضّلة

رائع

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

رائع

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

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

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

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

Kotlin

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

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

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

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

تخزين أزواج المفتاح/القيمة باستخدام 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, "settings").build();

القراءة من Preferences DataStore

بما أنّ Preferences 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. لمزيد من المعلومات حول تحديد مخطط 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 بكيفية قراءة وكتابة نوع البيانات. تأكَّد من تضمين قيمة تلقائية للمسلسل ليتم استخدامها في حال عدم إنشاء أي ملف بعد.
  2. استخدِم تفويض السمة الذي تم إنشاؤه بواسطة dataStore لإنشاء مثيل من DataStore<T>، حيث T هو النوع المحدّد في ملف proto. يمكنك استدعاء هذا الرمز مرة واحدة على مستوى أعلى في ملف Kotlin والوصول إليه من خلال تفويض هذه السمة في بقية تطبيقك. وتخبر المَعلمة filename واجهة 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() {
    return Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (InvalidProtocolBufferException exception) {
      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): 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 واجهة برمجة تطبيقات لمعالجة تلف البيانات يمكن أن تساعدك في استرداد البيانات بشكل سليم في مثل هذه الحالة وتجنُّب عرض الاستثناء. عند ضبط معالج التلف، يستبدل الملف التالف بملف جديد يحتوي على قيمة تلقائية محددة مسبقًا.

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

نماذج

المدوّنات

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