‫DataStore   جزء من Android Jetpack.

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

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

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

‫DataStore API

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

  1. ‫Flow يمكن استخدامه لقراءة البيانات من 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 نشطة لملف معيّن في العملية نفسها، سيؤدي ذلك إلى ظهور 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>(
                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(t)
                .encodeToByteArray()
        )
    }
}

‫Proto DataStore

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

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

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

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

حدِّد فئة تنفّذ 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 من خلال هذه السمة في بقية تطبيقك. يسهّل ذلك الحفاظ على مخزن البيانات ككائن أحادي. المَعلمة الإلزامية 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,
)

‫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.exampleCounter
}

استخدِم 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.copy { exampleCounter = exampleCounter + 1 }
    }
}

استخدام 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 أو Proto DataStore.

@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>(
                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(t)
                .encodeToByteArray()
        )
    }
}

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

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.filesDir.path}/time.pb")
    },
    corruptionHandler = null
)

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

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

تستدعي الخدمة بشكل دوري 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) }
)

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

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

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

مراجع إضافية

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

نماذج

المدوّنات

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