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.
Jetpack DataStore هي حلّ لتخزين البيانات يتيح لك تخزين أزواج المفتاح والقيمة أو العناصر المكتوبة باستخدام مخازن البروتوكول. يستخدم DataStore إجراءات Kotlin الروتينية المتزامنة وFlow لتخزين البيانات بشكل غير متزامن ومتسق ومعاملاتي.
إذا كنت تستخدم SharedPreferences لتخزين البيانات، ننصحك بنقل البيانات إلى DataStore بدلاً من ذلك.
DataStore API
توفّر واجهة DataStore واجهة برمجة التطبيقات التالية:
تدفّق يمكن استخدامه لقراءة البيانات من DataStore
val data: Flow<T>دالة لتعديل البيانات في 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.1.7" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Kotlin
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation("androidx.datastore:datastore-preferences:1.1.7") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
لإضافة دعم اختياري لـ RxJava، أضِف التبعيات التالية:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") }
DataStore
أضِف الأسطر التالية إلى جزء التبعيات في ملف Gradle:
Groovy
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation "androidx.datastore:datastore:1.1.7" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-core:1.1.7" }
Kotlin
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation("androidx.datastore:datastore:1.1.7") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-core:1.1.7") }
أضِف التبعيات الاختيارية التالية لتوفير دعم RxJava:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") }
لتسلسل المحتوى، أضِف التبعيات لتسلسل 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 بشكل صحيح، يجب دائمًا مراعاة القواعد التالية:
لا تنشئ أبدًا أكثر من مثيل واحد من
DataStoreلملف معيّن في العملية نفسها. وقد يؤدي ذلك إلى إيقاف جميع وظائف DataStore. إذا كانت هناك عدة DataStore نشطة لملف معيّن في العملية نفسها، ستعرض DataStore الخطأIllegalStateExceptionعند قراءة البيانات أو تعديلها.يجب أن يكون النوع العام
DataStore<T>غير قابل للتغيير. يؤدي تغيير نوع مستخدَم في DataStore إلى إبطال الاتساق الذي يوفّره DataStore وإنشاء أخطاء محتملة خطيرة يصعب رصدها. ننصحك باستخدام مخازن مؤقتة للبروتوكول، ما يساعد في ضمان عدم التغيير، وتوفير واجهة برمجة تطبيقات واضحة، وتنفيذ تسلسل فعال.لا تخلط بين استخدامات
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 من خلال هذه السمة في بقية تطبيقك. يسهّل ذلك إبقاء DataStore ككائن فردي.
بدلاً من ذلك، استخدِم RxPreferenceDataStoreBuilder إذا كنت تستخدم RxJava.
المَعلمة الإلزامية 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 باسم فئة التسلسل التي تم تحديدها في الخطوة 1.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.json",
serializer = SettingsSerializer,
)
Proto DataStore
استخدِم تفويض السمة الذي تم إنشاؤه بواسطة dataStore لإنشاء مثيل من DataStore<T>، حيث T هو النوع المحدّد في ملف proto. يمكنك استدعاء هذه الدالة مرة واحدة على مستوى أعلى في ملف Kotlin، والوصول إليها من خلال تفويض هذه السمة في بقية تطبيقك. وتخبر المَعلمة fileName مكتبة DataStore بالملف الذي يجب استخدامه لتخزين البيانات، بينما تخبر المَعلمة serializer مكتبة DataStore باسم فئة التسلسل التي تم تحديدها في الخطوة 1.
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
}
الكتابة في 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 }
}
}
نموذج الإنشاء
يمكنك وضع هذه الدوال معًا في فئة واستخدامها في تطبيق Compose.
Preferences DataStore
يمكننا الآن وضع هذه الدوال في فئة باسم PreferencesDataStore واستخدامها في تطبيق Compose.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }
// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(
onClick = {
coroutineScope.launch { preferencesDataStore.incrementCounter() }
}
) {
Text("increment")
}
JSON DataStore
يمكننا الآن وضع هذه الدوال في فئة باسم JSONDataStore واستخدامها في تطبيق Compose.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }
// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
Text("increment")
}
Proto DataStore
يمكننا الآن وضع هذه الدوال في فئة باسم ProtoDataStore واستخدامها في تطبيق Compose.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }
// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
Text("increment")
}
استخدام 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 ما يلي:
- لا تعرض عمليات القراءة سوى البيانات التي تم حفظها على القرص.
- الاتّساق بعد الكتابة
- تتم كتابة البيانات بشكل متسلسل.
- لا يتم حظر عمليات القراءة بسبب عمليات الكتابة.
لنفترض أنّ لديك تطبيقًا نموذجيًا يتضمّن خدمة ونشاطًا، حيث تعمل الخدمة في عملية منفصلة وتعدّل DataStore بشكل دوري.
يستخدم هذا المثال مخزن بيانات JSON، ولكن يمكنك أيضًا استخدام مخزن بيانات الإعدادات المفضّلة أو مخزن بيانات proto.
@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.cacheDir.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.cacheDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
تقديم ملاحظات
يمكنك مشاركة ملاحظاتك وأفكارك معنا من خلال المَراجع التالية:
- أداة تتبُّع المشاكل:
- الإبلاغ عن المشاكل لنتمكّن من إصلاح الأخطاء
مراجع إضافية
لمزيد من المعلومات حول Jetpack DataStore، اطّلِع على المراجع الإضافية التالية:
نماذج
المدوّنات
الدروس التطبيقية حول الترميز
مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- تحميل البيانات المقسّمة إلى صفحات وعرضها
- نظرة عامة على LiveData
- التصاميم وتعبيرات الربط