الحالة في التطبيق هي أي قيمة يمكن أن تتغير بمرور الوقت. وهذا تعريف واسع جدًا يشمل كل شيء، بدءًا من قاعدة بيانات Room إلى متغير في فئة.
تعرض جميع تطبيقات Android الحالة للمستخدم، وفي ما يلي بعض الأمثلة على الحالة في تطبيقات Android:
- شريط معلومات يظهر عند تعذُّر إنشاء اتصال بالشبكة
- مشاركة في مدونة والتعليقات المرتبطة بها
- رسوم متحركة على شكل تموّجات على الأزرار يتم تشغيلها عندما ينقر المستخدم عليها
- ملصقات يمكن للمستخدم رسمها فوق صورة
تساعدك 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 في التركيب أثناء التركيب الأوّلي، ويتم عرض القيمة المخزَّنة أثناء إعادة التركيب. ويمكن استخدام 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.1")
}
أنيق
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.11.1"
}
-
subscribeAsState()هي دوال إضافية تحوّل مصادر البيانات التفاعلية في RxJava2 (مثلSingleوObservableوCompletable) إلىStateفي Compose.يجب توفُّر الاعتمادية التالية في ملف
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.11.1")
}
أنيق
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.11.1"
}
-
subscribeAsState()هي دوال إضافية تحوّل مصادر البيانات التفاعلية في RxJava3 (مثلSingleوObservableوCompletable) إلىStateفي Compose.يجب توفُّر الاعتمادية التالية في ملف
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.11.1")
}
أنيق
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.11.1"
}
الخادم المتتبِّع لحالة الاتصال مقابل الخادم غير المتتبِّع لحالة الاتصال
تُنشئ الدالة المركّبة التي تستخدم remember لتخزين عنصر حالة داخلية، ما يجعل الدالة المركّبة تحتفظ بالحالة. HelloContent هو مثال على عنصر قابل للإنشاء مع الاحتفاظ بالحالة لأنّه يحتفظ بحالة name ويعدّلها داخليًا. يمكن أن يكون ذلك مفيدًا في الحالات التي لا يحتاج فيها المتصل إلى التحكّم في الحالة ويمكنه استخدامها بدون الحاجة إلى إدارتها بنفسه. ومع ذلك، فإنّ الدوال البرمجية القابلة للإنشاء التي تتضمّن حالة داخلية تكون أقل قابلية لإعادة الاستخدام وأصعب في الاختبار.
العنصر القابل للإنشاء بدون حالة هو عنصر لا يحتفظ بأي حالة. وأسهل طريقة لتحقيق ذلك هي استخدام نقل القيمة.
عند إنشاء دوال مركّبة قابلة لإعادة الاستخدام، غالبًا ما تريد توفير إصدارين من الدالة المركّبة نفسها، أحدهما يتضمّن حالة والآخر لا يتضمّن حالة. تكون النسخة التي تحتفظ بالحالة مفيدة للمتصلين الذين لا يهمهم الحالة، بينما تكون النسخة التي لا تحتفظ بالحالة ضرورية للمتصلين الذين يحتاجون إلى التحكّم في الحالة أو رفعها.
نقل القيمة
نقل الحالة في Compose هو نمط لنقل الحالة إلى الدالة التي تستدعي الدالة المركّبة، وذلك لجعل الدالة المركّبة بلا حالة. النمط العام لنقل القيمة في Jetpack Compose هو استبدال مُتغيِّر الحالة بمعلَمتَين:
value: T: القيمة الحالية المطلوب عرضها-
onValueChange: (T) -> Unit: حدث يطلب تغيير القيمة، حيثTهي القيمة الجديدة المقترَحة
ومع ذلك، لا يقتصر الأمر على onValueChange، فإذا كانت الأحداث الأكثر تحديدًا مناسبة للعنصر القابل للإنشاء، عليك تحديدها باستخدام تعبيرات lambda.
تتضمّن الحالة التي يتم نقلها بهذه الطريقة بعض الخصائص المهمة:
- المصدر الوحيد للحقيقة: من خلال نقل الحالة بدلاً من تكرارها، نضمن توفّر مصدر واحد للحقيقة، ما يساعد في تجنُّب الأخطاء.
- التغليف: يمكن فقط للعناصر القابلة للإنشاء ذات الحالة تعديل حالتها، وهي تكون داخلية تمامًا.
- قابلة للمشاركة: يمكن مشاركة الحالة التي تم نقلها مع عناصر متعددة قابلة للإنشاء. إذا أردت قراءة
nameفي دالة مركّبة مختلفة، سيسمح لك النقل بفعل ذلك. - قابلة للاعتراض: يمكن للمتصلين بوظائف composable عديمة الحالة أن يقرروا تجاهل الأحداث أو تعديلها قبل تغيير الحالة.
- مفصول: يمكن تخزين حالة العناصر القابلة للإنشاء عديمة الحالة في أي مكان. على سبيل المثال، يمكن الآن نقل
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 بشكل متكرّر مع MutableState:
var name by remember { mutableStateOf("") }
في هذا المثال، يؤدي استخدام الدالة remember إلى بقاء قيمة MutableState بعد عمليات إعادة التركيب.
بشكل عام، تأخذ remember مَعلمة lambda calculation. وعند تشغيل remember للمرة الأولى، يتم استدعاء lambda calculation وتخزين نتيجتها. وخلال إعادة التركيب، تعرض remember القيمة التي تم تخزينها آخر مرة.
بالإضافة إلى حالة التخزين المؤقت، يمكنك أيضًا استخدام remember لتخزين أي كائن أو نتيجة عملية في Composition يصعب تهيئتها أو حسابها. قد لا تحتاج إلى تكرار هذه العملية الحسابية في كل عملية إعادة التكوين.
على سبيل المثال، إنشاء عنصر ShaderBrush، وهو عملية مكلفة:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
تخزِّن remember القيمة إلى أن تغادر التركيب، ولكن هناك طريقة لإبطال القيمة المخزّنة مؤقتًا. تتلقّى واجهة برمجة التطبيقات remember أيضًا المَعلمة key أو keys. إذا تغيّر أي من هذين المفتاحين، سيؤدي remember إلى إبطال ذاكرة التخزين المؤقت وإعادة تنفيذ كتلة lambda الخاصة بالحساب في المرة التالية التي تتم فيها إعادة إنشاء التركيب. تتيح لك هذه الآلية التحكّم في مدة بقاء عنصر في التركيب. يظل الحساب صالحًا إلى أن تتغير المدخلات، بدلاً من أن يظل صالحًا إلى أن تغادر القيمة المحفوظة التركيب.
توضّح الأمثلة التالية طريقة عمل هذه الآلية.
في هذا المقتطف، يتم إنشاء 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) ) { /* ... */ } }
في المقتطف التالي، يتم نقل الحالة إلى فئة عنصر الاحتفاظ بالحالة العادي
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 API هو برنامج تضمين حول remember يمكنه تخزين البيانات في Bundle. تتيح واجهة برمجة التطبيقات هذه الاحتفاظ بالحالة ليس فقط عند إعادة التكوين، ولكن أيضًا عند إعادة إنشاء النشاط وإيقاف العملية نهائيًا التي بدأها النظام. تتلقّى rememberSaveable المَعلمات input للغرض نفسه الذي تتلقّى من أجله remember المَعلمات keys. يتم إبطال صحة ذاكرة التخزين المؤقت عند تغيير أي من المدخلات. وفي المرة التالية التي تتم فيها إعادة التكوين، تعيد rememberSaveable تنفيذ كتلة lambda الخاصة بالحساب.
في المثال التالي، تخزِّن 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