الحالة في التطبيق هي أي قيمة يمكن أن تتغيّر بمرور الوقت. هذا تعريف واسع جدًا ويشمل كل شيء من قاعدة بيانات Room إلى متغيّر في فئة.
تعرض جميع تطبيقات Android حالة للمستخدم. في ما يلي بعض الأمثلة على الحالة في تطبيقات Android:
- شريط Snackbar يظهر عندما يتعذّر إنشاء اتصال بالشبكة
- منشور مدوّنة والتعليقات المرتبطة به
- رسوم متحركة على شكل تموّجات على الأزرار يتم تشغيلها عندما ينقر عليها المستخدم
- ملصقات يمكن للمستخدم رسمها فوق صورة
يساعدك Jetpack Compose في تحديد مكان تخزين الحالة وكيفية استخدامها في تطبيق Android بشكل واضح. يركّز هذا الدليل على العلاقة بين الحالة والعناصر المركّبة، وعلى واجهات برمجة التطبيقات التي يقدّمها Jetpack Compose للتعامل مع الحالة بسهولة أكبر.
الحالة والإنشاء
تعتمد Compose على طريقة التصريح، وبالتالي فإنّ الطريقة الوحيدة لتعديلها هي استدعاء العنصر المركّب نفسه باستخدام وسيطات جديدة. تمثّل هذه الوسيطات حالة واجهة المستخدم. في أي وقت يتم فيه تعديل حالة، تحدث عملية إعادة إنشاء. نتيجةً لذلك، لا يتم تعديل عناصر مثل TextField تلقائيًا كما هو الحال في طرق العرض الإلزامية المستندة إلى XML. يجب إخبار العنصر المركّب بالحالة الجديدة بشكل صريح لكي يتم تعديله وفقًا لذلك.
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
إذا شغّلت هذا الرمز وحاولت إدخال نص، ستلاحظ أنّه لا يحدث أي شيء. يرجع ذلك إلى أنّ TextField لا تعدّل نفسها، بل يتم تعديلها عندما تتغيّر مَعلمة value. يرجع ذلك إلى طريقة عمل الإنشاء وإعادة الإنشاء في Compose.
لمزيد من المعلومات عن الإنشاء الأولي وإعادة الإنشاء، يُرجى الاطّلاع على مقالة التفكير بطريقة Compose.
الحالة في العناصر المركّبة
يمكن أن تستخدم الدوال المركّبة واجهة برمجة التطبيقات
remember
لتخزين كائن في الذاكرة. يتم تخزين قيمة محسوبة من خلال remember في عنصر Composition أثناء الإنشاء الأولي، ويتم عرض القيمة المخزّنة أثناء إعادة الإنشاء.
يمكن استخدام remember لتخزين الكائنات القابلة للتغيير وغير القابلة للتغيير.
mutableStateOf
تنشئ عنصرًا قابلاً للمراقبة
MutableState<T>،
وهو نوع قابل للمراقبة مدمج مع وقت تشغيل Compose.
interface MutableState<T> : State<T> {
override var value: T
}
تؤدي أي تغييرات على value إلى جدولة إعادة إنشاء أي دوال مركّبة تقرأ value.
هناك ثلاث طرق للتعريف بكائن MutableState في دالة مركّبة:
val mutableState = remember { mutableStateOf(default) }var value by remember { mutableStateOf(default) }val (value, setValue) = remember { mutableStateOf(default) }
هذه التصريحات متكافئة، ويتم تقديمها كطريقة مختصرة لتسهيل استخدام الحالة في حالات مختلفة. عليك اختيار الطريقة التي تنتج الرمز الأسهل قراءةً في العنصر المركّب الذي تكتبه.
تتطلّب بنية التفويض by عمليات الاستيراد التالية:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
يمكنك استخدام القيمة التي تم تذكّرها كمعلَمة لعناصر مركّبة أخرى أو حتى كمنطق في عبارات لتغيير العناصر المركّبة التي يتم عرضها. على سبيل المثال، إذا كنت لا تريد عرض التحية إذا كان الاسم فارغًا، استخدِم الحالة في عبارة if:
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
على الرغم من أنّ remember تساعدك في الاحتفاظ بالحالة أثناء عمليات إعادة الإنشاء، لا يتم الاحتفاظ بالحالة أثناء تغييرات الإعدادات. لتحقيق ذلك، عليك استخدام rememberSaveable. تحفظ rememberSaveable تلقائيًا أي قيمة يمكن حفظها في Bundle. بالنسبة إلى القيم الأخرى، يمكنك تمرير كائن حافظة مخصّص.
أنواع الحالة الأخرى المتوافقة
لا تتطلّب Compose استخدام MutableState<T> للاحتفاظ بالحالة، بل تتيح استخدام أنواع أخرى قابلة للمراقبة. قبل قراءة نوع آخر قابل للمراقبة في
Compose، عليك تحويله إلى State<T> حتى تتمكّن العناصر المركّبة من
إعادة الإنشاء تلقائيًا عند تغيُّر الحالة.
تتضمّن Compose دوالاً لإنشاء State<T> من الأنواع الشائعة القابلة للمراقبة
المستخدَمة في تطبيقات Android. قبل استخدام عمليات الدمج هذه، أضِف الـ
عناصر المناسبة كما هو موضّح أدناه:
Flow:collectAsStateWithLifecycle()collectAsStateWithLifecycle()تجمع القيم منFlowبطريقة تراعي مراحل النشاط، ما يسمح لتطبيقك بـ الحفاظ على موارد التطبيق. تمثّل هذه الدالة أحدث قيمة تم إصدارها من الـ ComposeState. استخدِم واجهة برمجة التطبيقات هذه كطريقة مقترَحة لجمع التدفقات في تطبيقات Android.الاعتمادية التالية مطلوبة في ملف
build.gradle(يجب أن يكون الإصدار 2.6.0-beta01 أو إصدارًا أحدث):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
}
أنيق
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.10.0"
}
-
collectAsStateتشبهcollectAsStateWithLifecycle، لأنّها أيضًا تجمع القيم منFlowوتحوّلها إلىStateفي Compose.استخدِم
collectAsStateللرمز المستقل عن النظام الأساسي بدلاً منcollectAsStateWithLifecycle، التي لا تتوفّر إلا على Android.لا يلزم توفُّر أي اعتماديات إضافية لـ
collectAsState، لأنّها متاحة فيcompose-runtime. -
تبدأ
observeAsState()مراقبةLiveDataوتمثّل قيمها من خلالState.الاعتمادية التالية مطلوبة في الملف
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.11.0")
}
أنيق
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.11.0"
}
-
subscribeAsState()هي دوال إضافية تحوّل التدفقات التفاعلية في RxJava2 (مثلSingleوObservableوCompletable) إلىStateفي Compose.الاعتمادية التالية مطلوبة في الملف
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.11.0")
}
أنيق
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.11.0"
}
-
subscribeAsState()هي دوال إضافية تحوّل التدفقات التفاعلية في RxJava3 (مثلSingleوObservableوCompletable) إلىStateفي Compose.الاعتمادية التالية مطلوبة في الملف
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.11.0")
}
أنيق
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.11.0"
}
العناصر المركّبة التي تحتفظ بالحالة مقابل العناصر المركّبة التي لا تحتفظ بالحالة
ينشئ العنصر المركّب الذي يستخدم remember لتخزين كائن حالة داخلية، ما يجعل العنصر المركّب يحتفظ بالحالة. HelloContent هو مثال على دالة مركّبة تحتفظ بالحالة لأنّه يحتفظ بحالة name ويعدّلها داخليًا. يمكن أن يكون ذلك مفيدًا في الحالات التي لا يحتاج فيها المستدعي إلى التحكّم في الحالة ويمكنه استخدامها بدون الحاجة إلى إدارة الحالة بنفسه. ومع ذلك، تميل العناصر المركّبة التي تحتفظ بالحالة الداخلية إلى أن تكون أقل قابلية لإعادة الاستخدام وأكثر صعوبة في الاختبار.
العنصر المركّب الذي لا يحتفظ بالحالة هو عنصر مركّب لا يحتفظ بأي حالة. إحدى الطرق السهلة لتحقيق ذلك هي استخدام نقل القيمة .
أثناء تطوير عناصر مركّبة قابلة لإعادة الاستخدام، غالبًا ما تريد عرض كل من الإصدار الذي يحتفظ بالحالة والإصدار الذي لا يحتفظ بالحالة من العنصر المركّب نفسه. يكون الإصدار الذي يحتفظ بالحالة مناسبًا للمستدعين الذين لا يهتمون بالحالة، ويكون الإصدار الذي لا يحتفظ بالحالة ضروريًا للمستدعين الذين يحتاجون إلى التحكّم في الحالة أو نقلها.
نقل القيمة
نقل القيمة في Compose هو نمط لنقل الحالة إلى المستدعي الخاص بالعنصر المركّب لجعل العنصر المركّب لا يحتفظ بالحالة. النمط العام لنقل القيمة في Jetpack Compose هو استبدال متغيّر الحالة بمعلَمتَين:
value: T: القيمة الحالية التي سيتم عرضهاonValueChange: (T) -> Unit: حدث يطلب تغيير القيمة، حيثTهي القيمة الجديدة المقترَحة
ومع ذلك، لا يقتصر الأمر على onValueChange. إذا كانت الأحداث الأكثر تحديدًا مناسبة للعنصر المركّب، عليك تحديدها باستخدام تعبيرات لامدا.
تتضمّن الحالة التي يتم نقلها بهذه الطريقة بعض الخصائص المهمة:
- المصدر الوحيد للحقيقة: من خلال نقل الحالة بدلاً من تكرارها، نضمن وجود مصدر واحد فقط للحقيقة. يساعد ذلك في تجنُّب الأخطاء.
- التغليف: يمكن فقط للعناصر المركّبة التي تحتفظ بالحالة تعديل حالتها. وهي حالة داخلية تمامًا.
- إمكانية المشاركة: يمكن مشاركة الحالة التي تم نقلها مع عدة عناصر مركّبة. إذا أردت قراءة
nameفي دالة مركّبة مختلفة، سيسمح لك نقل القيمة بذلك. - إمكانية الاعتراض: يمكن للمستدعين إلى العناصر المركّبة التي لا تحتفظ بالحالة اختيار تجاهل الأحداث أو تعديلها قبل تغيير الحالة.
- الفصل: يمكن تخزين حالة العناصر المركّبة التي لا تحتفظ بالحالة في أي مكان. على سبيل المثال، أصبح من الممكن الآن نقل
nameإلىViewModel.
في المثال، يمكنك استخراج name وonValueChange من HelloContent ونقلهما إلى أعلى الشجرة إلى دالة مركّبة HelloScreen تستدعي HelloContent.
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
من خلال نقل الحالة خارج HelloContent، يصبح من الأسهل فهم العنصر المركّب وإعادة استخدامه في مواقف مختلفة واختباره. تم فصل HelloContent عن طريقة تخزين حالته. يعني الفصل أنّه إذا عدّلت HelloScreen أو استبدلته، لن تحتاج إلى تغيير طريقة تنفيذ HelloContent.
يُطلق على النمط الذي تنتقل فيه الحالة إلى الأسفل وتنتقل فيه الأحداث إلى الأعلى اسم تدفق البيانات أحادي الاتجاه. في هذه الحالة، تنتقل الحالة إلى الأسفل من HelloScreen إلى HelloContent وتنتقل الأحداث إلى الأعلى من HelloContent إلى HelloScreen. من خلال اتّباع تدفق البيانات أحادي الاتجاه، يمكنك فصل العناصر المركّبة التي تعرض الحالة في واجهة المستخدم عن أجزاء تطبيقك التي تخزّن الحالة وتغيّرها.
لمزيد من المعلومات، يُرجى الاطّلاع على صفحة مكان نقل الحالة.
استعادة الحالة في Compose
تعمل واجهة برمجة التطبيقات rememberSaveable بطريقة مشابهة لـ remember لأنّها
تحتفظ بالحالة أثناء عمليات إعادة الإنشاء، وأيضًا أثناء إعادة إنشاء النشاط أو العملية
باستخدام آلية حالة المثيل المحفوظة. على سبيل المثال، يحدث ذلك عند تدوير الشاشة.
طرق تخزين الحالة
يتم حفظ جميع أنواع البيانات التي تتم إضافتها إلى Bundle تلقائيًا. إذا أردت حفظ شيء لا يمكن إضافته إلى Bundle، هناك عدة خيارات.
Parcelize
أبسط حلّ هو إضافة التعليق التوضيحي
@Parcelize
إلى الكائن. يصبح الكائن قابلاً للتجزئة، ويمكن تجميعه. على سبيل المثال، ينشئ هذا الرمز نوع بيانات City قابلاً للتجزئة ويحفظه في الحالة.
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
إذا لم يكن @Parcelize مناسبًا لسبب ما، يمكنك استخدام mapSaver لتحديد القاعدة الخاصة بك لتحويل كائن إلى مجموعة من القيم التي يمكن للنظام حفظها في Bundle.
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
ListSaver
لتجنُّب الحاجة إلى تحديد المفاتيح للخريطة، يمكنك أيضًا استخدام listSaver واستخدام مؤشراتها كمفاتيح:
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
حاملو الحالة في Compose
يمكن إدارة عملية نقل القيمة البسيطة في الدوال المركّبة نفسها. ومع ذلك، إذا زادت كمية الحالة التي يجب تتبُّعها، أو إذا ظهر المنطق الذي يجب تنفيذه في الدوال المركّبة، من المستحسن تفويض مسؤوليات المنطق والحالة إلى فئات أخرى: حاملو الحالة.
لمزيد من المعلومات، يُرجى الاطّلاع على مستندات نقل القيمة في Compose أو، بشكل أكثر عمومية، صفحة حاملو الحالة وحالة واجهة المستخدم في دليل البنية.
إعادة تشغيل عمليات حساب `remember` عند تغيُّر المفاتيح
كثيرًا ما تُستخدم واجهة برمجة التطبيقات remember مع MutableState:
var name by remember { mutableStateOf("") }
هنا، يؤدي استخدام الدالة remember إلى بقاء قيمة MutableState أثناء عمليات إعادة الإنشاء.
بشكل عام، تأخذ remember مَعلمة لامدا calculation. عند تشغيل remember للمرة الأولى، تستدعي لامدا calculation وتخزّن نتيجتها. أثناء إعادة الإنشاء، تعرض remember القيمة التي تم تخزينها آخر مرة.
بالإضافة إلى تخزين الحالة مؤقتًا، يمكنك أيضًا استخدام remember لتخزين أي كائن أو نتيجة عملية في عنصر Composition يكون من الصعب تهيئته أو حسابه. قد لا تريد تكرار هذا الحساب في كل عملية إعادة إنشاء.
أحد الأمثلة هو إنشاء كائن ShaderBrush هذا، وهي عملية مكلفة:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
تخزّن remember القيمة إلى أن تغادر عنصر Composition. ومع ذلك، هناك طريقة لإبطال القيمة المخزّنة مؤقتًا. تأخذ واجهة برمجة التطبيقات remember أيضًا مَعلمة key أو
keys. إذا تغيّرت أي من هذه المفاتيح، ففي المرة التالية التي يعيد فيها العنصر المركّب إنشاء نفسه، remember تبطل ذاكرة التخزين المؤقت وتعيد تنفيذ كتلة لامدا للحساب. تمنحك هذه الآلية التحكّم في مدة بقاء الكائن في عنصر Composition. تظل عملية الحساب صالحة إلى أن تتغيّر المدخلات، بدلاً من أن تظل صالحة إلى أن تغادر القيمة التي تم تذكّرها عنصر Composition.
توضّح الأمثلة التالية طريقة عمل هذه الآلية.
في هذا المقتطف، يتم إنشاء ShaderBrush واستخدامه كخلفية
طلاء لعنصر Box المركّب. remember تخزّن مثيل ShaderBrush
لأنّ إعادة إنشائه مكلفة، كما هو موضّح سابقًا. تأخذ remember السمة avatarRes كمَعلمة key1، وهي صورة الخلفية المحدّدة. إذا تغيّرت avatarRes، يعيد الفرشاة إنشاء نفسها باستخدام الصورة الجديدة، ويعاد تطبيقها على Box. يمكن أن يحدث ذلك عندما يختار المستخدم صورة أخرى لتكون الخلفية من أداة اختيار.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
في المقتطف التالي، يتم نقل الحالة إلى فئة عنصر الاحتفاظ بالحالة العادية plain state holder class
MyAppState. تعرض هذه الفئة دالة rememberMyAppState لتهيئة مثيل من الفئة باستخدام remember. إنّ عرض مثل هذه الدوال لإنشاء مثيل يبقى أثناء عمليات إعادة الإنشاء هو نمط شائع في Compose. تتلقّى الدالة
rememberMyAppState السمة windowSizeClass، التي تعمل كـ
المَعلمة key لـ remember. إذا تغيّرت هذه المَعلمة، يحتاج التطبيق إلى إعادة إنشاء فئة عنصر الاحتفاظ بالحالة العادية باستخدام أحدث قيمة. قد يحدث ذلك، على سبيل المثال، إذا قام المستخدم بتدوير الجهاز.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
تستخدم Compose تنفيذ الدالة equals للفئة لتحديد ما إذا كان قد تغيّر مفتاح وإبطال القيمة المخزّنة.
تخزين الحالة باستخدام المفاتيح بعد إعادة الإنشاء
واجهة برمجة التطبيقات rememberSaveable هي برنامج تضمين حول remember يمكنه تخزين
البيانات في Bundle. تسمح واجهة برمجة التطبيقات هذه ببقاء الحالة ليس فقط أثناء إعادة الإنشاء، ولكن أيضًا أثناء إعادة إنشاء النشاط وإيقاف العملية نهائيًا من قِبل النظام.
تتلقّى rememberSaveable مَعلمات input للغرض نفسه الذي تتلقّى من أجله
remember السمة keys. يتم إبطال ذاكرة التخزين المؤقت عند تغيُّر أي من المدخلات. في المرة التالية التي يعيد فيها العنصر المركّب إنشاء نفسه، تعيد rememberSaveable تنفيذ كتلة لامدا للحساب.
في المثال التالي، تخزّن rememberSaveable السمة userTypedQuery إلى أن تتغيّر typedQuery:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
مزيد من المعلومات
لمزيد من المعلومات عن الحالة وJetpack Compose، يُرجى الاطّلاع على المراجع الإضافية التالية.
نماذج
اختبارات الرموز
الفيديوهات
المدوّنات
مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- تصميم واجهة مستخدم Compose
- حفظ حالة واجهة المستخدم في Compose
- الآثار الجانبية في Compose