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

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

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

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

DataStore API

توفّر واجهة DataStore واجهة برمجة التطبيقات التالية:

  1. تدفّق يمكن استخدامه لقراءة البيانات من DataStore

    val data: Flow<T>
    
  2. دالة لتعديل البيانات في DataStore

    suspend updateData(transform: suspend (t) -> T)
    

إعدادات DataStore

إذا كنت تريد تخزين البيانات والوصول إليها باستخدام المفاتيح، استخدِم تنفيذ Preferences DataStore الذي لا يتطلّب مخططًا محدّدًا مسبقًا ولا يوفّر منع أخطاء الكتابة. تتضمّن واجهة برمجة تطبيقات مشابهة لواجهة SharedPreferences، ولكنها لا تتضمّن عيوبًا مرتبطة بالإعدادات المفضّلة المشترَكة.

تتيح لك DataStore الاحتفاظ بفئات مخصّصة. لإجراء ذلك، يجب تحديد مخطط للبيانات وتوفير Serializer لتحويلها إلى تنسيق يمكن حفظه. يمكنك اختيار استخدام Protocol Buffers أو JSON أو أي استراتيجية نشر على نحو متسلسِل أخرى.

الإعداد

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

Preferences DataStore

أضِف الأسطر التالية إلى جزء التبعيات في ملف Gradle:

Groovy

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

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.2.1"
    }
    

Kotlin

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

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.2.1")
    }
    

لإضافة دعم اختياري لـ RxJava، أضِف التبعيات التالية:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.2.1")

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

DataStore

أضِف الأسطر التالية إلى جزء التبعيات في ملف Gradle:

Groovy

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.2.1"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.2.1"
    }
    

Kotlin

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.2.1")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.2.1")
    }
    

أضِف التبعيات الاختيارية التالية لتوفير دعم RxJava:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.2.1"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.2.1")

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

لتسلسل المحتوى، أضِف الاعتماديات لتسلسل Protocol Buffers أو JSON.

تسلسل JSON

لاستخدام تسلسل JSON، أضِف ما يلي إلى ملف Gradle:

Groovy

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Kotlin

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

نشر Protobuf على نحو متسلسِل

لاستخدام تسلسل Protobuf، أضِف ما يلي إلى ملف Gradle:

Groovy

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Kotlin

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

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

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

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

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

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

تعريف البيانات

‫Preferences DataStore

حدِّد مفتاحًا سيتم استخدامه لتخزين البيانات على القرص.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

بالنسبة إلى مخزن بيانات JSON، أضِف تعليقًا توضيحيًا @Serialization إلى البيانات التي تريد الاحتفاظ بها.

@Serializable data class Settings(val exampleCounter: Int)

حدِّد فئة تنفّذ Serializer<T>، حيث T هو نوع الفئة التي أضفت إليها التعليق التوضيحي السابق. احرص على تضمين قيمة تلقائية سيتم استخدامها في أداة التسلسل إذا لم يتم إنشاء ملف بعد.

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 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())
    }
}

Proto DataStore

يستخدم تنفيذ Proto DataStore كلاً من DataStore وبروتوكولات المخزن المؤقت لتخزين الكائنات المكتوبة على القرص.

يتطلّب Proto DataStore مخططًا محدّدًا مسبقًا في ملف proto ضمن الدليل app/src/main/proto/. يحدّد هذا المخطط نوع العناصر التي تحتفظ بها في Proto DataStore. لمزيد من المعلومات حول تحديد مخطط أولي، يُرجى الاطّلاع على دليل لغة protobuf.

أضِف ملفًا باسم settings.proto داخل المجلد src/main/proto:

syntax = "proto3";

option java_package = "com.example.datastoresampleapp";
option java_multiple_files = true;

message Settings {
  int32 counter = 1;
  bool foo = 2;
}

عرِّف فئة تنفّذ Serializer<T>، حيث T هو النوع المحدّد في ملف proto. يحدّد فئة التسلسل هذه الطريقة التي يقرأ بها DataStore ويكتب نوع البيانات. احرِص على تضمين قيمة تلقائية للمسلسل لاستخدامها في حال عدم إنشاء ملف بعد.

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) {
        return t.writeTo(output)
    }
}

إنشاء DataStore

عليك تحديد اسم للملف المستخدَم لتخزين البيانات بشكل دائم.

‫Preferences DataStore

يستخدم تنفيذ Preferences DataStore الفئتين DataStore وPreferences لتخزين أزواج المفتاح/القيمة على القرص. استخدِم تفويض السمة الذي تم إنشاؤه بواسطة preferencesDataStore لإنشاء مثيل من DataStore<Preferences>. يمكنك استدعاؤها مرة واحدة على مستوى أعلى في ملف Kotlin. يمكنك الوصول إلى DataStore من خلال هذه السمة في بقية تطبيقك. يسهّل ذلك إبقاء DataStore ككائن فردي. المَعلمة الإلزامية name هي اسم Preferences DataStore.

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

JSON DataStore

استخدِم مفوّض السمة الذي تم إنشاؤه بواسطة dataStore لإنشاء مثيل من DataStore<T>، حيث T هي فئة البيانات القابلة للتسلسل. يمكنك استدعاء هذه الدالة مرة واحدة في المستوى الأعلى من ملف Kotlin والوصول إليها من خلال تفويض هذه السمة في بقية تطبيقك. يحدّد المَعلمة fileName لـ DataStore الملف الذي سيتم استخدامه لتخزين البيانات، وتحدّد المَعلمة serializer لـ DataStore اسم فئة التسلسل التي تم تحديدها سابقًا.

val Context.dataStore: DataStore<Settings> by
    dataStore(
        fileName = "settings.json",
        serializer = SettingsSerializer,
        scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
    )

Proto DataStore

استخدِم تفويض السمة الذي تم إنشاؤه بواسطة dataStore لإنشاء مثيل من DataStore<T>، حيث T هو النوع المحدّد في ملف proto. يمكنك استدعاء هذه الدالة مرة واحدة على مستوى أعلى في ملف Kotlin، والوصول إليها من خلال تفويض هذه السمة في بقية تطبيقك. وتخبر المَعلمة fileName DataStore بالملف الذي سيتم استخدامه لتخزين البيانات، بينما تخبر المَعلمة serializer DataStore باسم فئة التسلسل التي تم تحديدها سابقًا.

val Context.dataStore: DataStore<Settings> by
    dataStore(fileName = "settings.pb", serializer = SettingsSerializer)

القراءة من DataStore

عليك تحديد اسم للملف المستخدَم لتخزين البيانات بشكل دائم.

‫Preferences DataStore

بما أنّ Preferences DataStore لا يستخدم مخططًا محدّدًا مسبقًا، عليك استخدام دالة نوع المفتاح المناسبة لتحديد مفتاح لكل قيمة تريد تخزينها في مثيل DataStore<Preferences>. على سبيل المثال، لتحديد مفتاح لقيمة عدد صحيح، استخدِم intPreferencesKey. بعد ذلك، استخدِم السمة DataStore.data لعرض القيمة المخزّنة المناسبة باستخدام Flow.

fun counterFlow(): Flow<Int> =
    context.dataStore.data.map { preferences -> preferences[EXAMPLE_COUNTER] ?: 0 }

JSON DataStore

استخدِم DataStore.data لعرض Flow للسمة المناسبة من العنصر المخزَّن.

fun counterFlow(): Flow<Int> =
    context.dataStore.data.map { settings -> settings.exampleCounter }

Proto DataStore

استخدِم DataStore.data لعرض Flow للسمة المناسبة من العنصر المخزَّن.

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings -> settings.counter }

استخدِم collectAsStateWithLifecycle لاستهلاك Flow الذي تم إنشاؤه بواسطة ViewModel في دالة مركّبة. يؤدي ذلك إلى تحويل DataStore Flow بأمان إلى Compose State الذي يؤدي إلى إعادة الإنشاء.

@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
  val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
  Text(text = "Example counter: ${counter}")
}

لمزيد من المعلومات حول collectAsStateWithLifecycle، اطّلِع على الحالة وJetpack Compose.

الكتابة إلى DataStore

توفّر DataStore الدالة updateData التي تعدّل بشكل متسق كائنًا مخزّنًا. تمنحك updateData الحالة الحالية للبيانات كنموذج لنوع البيانات، وتعدّل البيانات بشكل متسق في عملية قراءة وكتابة وتعديل واحدة. يتم التعامل مع كل الرموز في كتلة updateData على أنّها عملية واحدة.

‫Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

JSON DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.toBuilder().setCounter(settings.counter + 1).build()
    }
}

استخدام DataStore في تطبيق يستخدِم Compose

لاستخدام DataStore في تطبيق Compose، اتّبِع إرشادات بنية تطبيقات Android من خلال إبقاء عمليات DataStore في طبقة البيانات (مثل المستودع) وعرض البيانات على واجهة المستخدم من خلال ViewModel.

تجنَّب القراءة من DataStore أو الكتابة إليه مباشرةً ضمن الدوال القابلة للإنشاء.

  1. عرض DataStore من خلال ViewModel مرِّر المستودع (الذي يغلّف DataStore) إلى ViewModel وحوِّل Flow إلى StateFlow ليتمكّن واجهة المستخدم من مراقبته، كما هو موضّح في المقتطف التالي:

    class SettingsViewModel(
        private val userPreferencesRepository: UserPreferencesRepository
    ) : ViewModel() {
    
        // Expose the DataStore flow as a StateFlow for Compose
        val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = UserSettings.getDefaultInstance()
            )
    
        fun updateCounter(newValue: Int) {
            viewModelScope.launch {
                userPreferencesRepository.updateCounter(newValue)
            }
        }
    }
    
  2. مراقبة الكتابة من العنصر القابل للإنشاء استخدِم collectAsStateWithLifecycle لمراقبة StateFlow بأمان في واجهة المستخدم، واستدعِ دوال ViewModel للتعامل مع عمليات الكتابة، كما هو موضّح في المقتطف التالي:

    @Composable
    fun SettingsScreen(
        viewModel: SettingsViewModel = viewModel()
    ) {
        // Safely collect the state
        val settings by viewModel.userSettings.collectAsStateWithLifecycle()
    
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Current counter: ${settings.counter}")
    
            Spacer(modifier = Modifier.height(8.dp))
    
            Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) {
                Text("Increment Counter")
            }
        }
    }
    

استخدام DataStore في الرموز البرمجية المتعددة العمليات

يمكنك ضبط DataStore للوصول إلى البيانات نفسها في عمليات مختلفة باستخدام خصائص اتساق البيانات نفسها كما هو الحال في عملية واحدة. على وجه الخصوص، يوفّر DataStore الخصائص التالية:

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

لنفترض أنّ لديك تطبيقًا نموذجيًا يتضمّن خدمة ونشاطًا، حيث تعمل الخدمة في عملية منفصلة وتعدّل DataStore بشكل دوري.

يستخدم هذا المثال مخزن بيانات JSON، ولكن يمكنك أيضًا استخدام Preferences أو ProtoDataStore.

@Serializable data class Time(val lastUpdateMillis: Long)

يخبر برنامج التسلسل DataStore بكيفية قراءة نوع البيانات وكتابته. تأكَّد من تضمين قيمة تلقائية للمسلسل لاستخدامها في حال عدم إنشاء ملف بعد. في ما يلي مثال على عملية تنفيذ باستخدام kotlinx.serialization:

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

    override suspend fun readFrom(input: InputStream): Time =
        try {
            Json.decodeFromString(Time.serializer(), input.readBytes().decodeToString())
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read Time", serialization)
        }

    override suspend fun writeTo(t: Time, output: OutputStream) {
        output.write(Json.encodeToString(Time.serializer(), t).encodeToByteArray())
    }
}

لكي تتمكّن من استخدام DataStore في عمليات مختلفة، عليك إنشاء عنصر DataStore باستخدام MultiProcessDataStoreFactory لكلّ من التطبيق ورمز الخدمة:

val dataStore =
    MultiProcessDataStoreFactory.create(
        serializer = TimeSerializer,
        produceFile = { context.dataStoreFile("time.pb") },
        corruptionHandler = null,
    )

أضِف ما يلي إلى AndroidManifiest.xml:

<service
    android:name="com.example.datastore.snippets.TimestampUpdateService"
    android:exported="false"
    android:process=":service" />

تتصل الخدمة بشكل دوري بـ updateLastUpdateTime، الذي يكتب إلى مخزن البيانات باستخدام updateData.

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time -> time.copy(lastUpdateMillis = System.currentTimeMillis()) }
}

يقرأ التطبيق القيمة التي كتبتها الخدمة باستخدام تدفق البيانات:

fun timeFlow(): Flow<Long> = dataStore.data.map { time -> time.lastUpdateMillis }

يمكننا الآن وضع كل هذه الدوال معًا في فئة باسم MultiProcessDataStore واستخدامها في تطبيق.

إليك رمز الخدمة:

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

ورمز التطبيق:

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by
    multiProcessDataStore
        .timeFlow()
        .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(text = "Last updated: $lastUpdateTime", fontSize = 25.sp)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose { context.stopService(serviceIntent) }
}

يمكنك استخدام ميزة تضمين التبعيات في 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.filesDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

الاحتفاظ بنسخة احتياطية من بيانات الجهاز واستعادتها

يتم تخزين ملفات Preferences DataStore (*.preferences_pb) وملفات Proto DataStore في دليل files/datastore/ الخاص بالتطبيق. يتم تضمين هذه الملفات تلقائيًا في الاحتفاظ التلقائي بنسخة احتياطية على Android وعمليات النقل من جهاز إلى جهاز.

ضبط قواعد الاحتفاظ بنسخة احتياطية من DataStore

إذا كان DataStore يحتوي على إعدادات مفضّلة غير حسّاسة (مثل سمة المستخدم أو علامات الميزات) إلى جانب البيانات الحسّاسة، عليك فصلها في ملفات DataStore منفصلة وضبط res/xml/data_extraction_rules.xml:

<data-extraction-rules>
    <cloud-backup>
        <!-- Include general settings -->
        <include domain="file" path="datastore/user_settings.preferences_pb"/>
        <!-- Exclude sensitive local state -->
        <exclude domain="file" path="datastore/secure_state.preferences_pb"/>
    </cloud-backup>
    <device-to-device>
        <!-- Transfer settings during device-to-device transfer -->
        <include domain="file" path="datastore/"/>
    </device-to-device>
</data-extraction-rules>

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

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

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

مراجع إضافية

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

نماذج

المدوّنات

اختبارات الرموز