الحالة في التطبيق هي أي قيمة يمكن أن تتغير بمرور الوقت. وهذا تعريف واسع جدًا يشمل كل شيء، بدءًا من قاعدة بيانات 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 في 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.9.4")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.9.4"
}
-
تشبه الدالة
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.9.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.9.3"
}
-
subscribeAsState()هي دوال إضافية تحوّل مصادر البيانات التفاعلية في RxJava2 (مثلSingleوObservableوCompletable) إلىStateفي Compose.يجب توفُّر الاعتمادية التالية في ملف
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.9.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.9.3"
}
-
subscribeAsState()هي دوال إضافية تحوّل مصادر البيانات التفاعلية في RxJava3 (مثلSingleوObservableوCompletable) إلىStateفي Compose.يجب توفُّر الاعتمادية التالية في ملف
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.9.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.9.3"
}
الخادم المتتبِّع لحالة الاتصال مقابل الخادم غير المتتبِّع لحالة الاتصال
تنشئ الدالة البرمجية القابلة للإنشاء التي تستخدم remember لتخزين عنصر حالة داخلية،
ما يجعل الدالة البرمجية القابلة للإنشاء تحتوي على حالة. HelloContent هو مثال على دالة برمجية قابلة للإنشاء مع الاحتفاظ بالحالة لأنّها تحتفظ بحالة name وتعدّلها داخليًا. يمكن أن يكون ذلك مفيدًا في الحالات التي لا يحتاج فيها المتصل إلى التحكّم في الحالة ويمكنه استخدامها بدون الحاجة إلى إدارتها بنفسه. ومع ذلك، فإنّ العناصر القابلة للإنشاء التي تتضمّن حالة داخلية تكون أقل قابلية لإعادة الاستخدام وأكثر صعوبة في الاختبار.
العنصر القابل للإنشاء غير الاحتفاظ بالحالة هو عنصر قابل للإنشاء لا يحتفظ بأي حالة. تتمثّل إحدى الطرق السهلة لتحقيق ذلك في استخدام نقل الحالة.
عند إنشاء دوال برمجية قابلة للإنشاء وقابلة لإعادة الاستخدام، غالبًا ما تريد توفير إصدارين من الدالة البرمجية القابلة للإنشاء نفسها، أحدهما يتضمّن حالة والآخر لا يتضمّن حالة. تكون النسخة التي تحتفظ بالحالة مناسبة للمتصلين الذين لا يهمهم الحالة، بينما تكون النسخة التي لا تحتفظ بالحالة ضرورية للمتصلين الذين يحتاجون إلى التحكّم في الحالة أو رفعها.
نقل القيمة
في Compose، يشير مصطلح "نقل الحالة إلى أعلى" إلى نمط نقل الحالة إلى العنصر الذي يستدعي العنصر القابل للإنشاء، وذلك لجعل العنصر القابل للإنشاء بلا حالة. النمط العام لرفع الحالة في Jetpack Compose هو استبدال متغيّر الحالة بمعلَمتَين:
value: T: القيمة الحالية التي سيتم عرضها-
onValueChange: (T) -> Unit: حدث يطلب تغيير القيمة، حيثTهي القيمة الجديدة المقترَحة
ومع ذلك، لا يقتصر الأمر على onValueChange. إذا كانت الأحداث الأكثر تحديدًا مناسبة للعنصر القابل للإنشاء، عليك تحديدها باستخدام تعبيرات lambda.
تتضمّن الحالة التي يتم نقلها بهذه الطريقة بعض الخصائص المهمة:
- مصدر واحد للحقيقة: من خلال نقل الحالة بدلاً من تكرارها، نضمن توفّر مصدر واحد للحقيقة. يساعد ذلك في تجنُّب الأخطاء.
- التغليف: يمكن فقط للعناصر القابلة للإنشاء ذات الحالة تعديل حالتها. وهي داخلية بالكامل.
- قابلة للمشاركة: يمكن مشاركة الحالة التي تم نقلها إلى مستوى أعلى مع عناصر متعددة قابلة للإنشاء. إذا أردت عرض
nameفي عنصر قابل للإنشاء مختلف، سيسمح لك النقل إلى مستوى أعلى بذلك. - قابلة للاعتراض: يمكن للمتصلين بوظائف Compose غير الاحتفاظ بالحالة أن يقرروا تجاهل الأحداث أو تعديلها قبل تغيير الحالة.
- مفصول: يمكن تخزين حالة العناصر القابلة للإنشاء بدون حالة في أي مكان. على سبيل المثال، أصبح من الممكن الآن نقل
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 إلى العنصر. يصبح العنصر قابلاً للتجزئة، ويمكن تجميعه. على سبيل المثال، ينشئ هذا الرمز نوع بيانات 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
للمرة الأولى، يتم استدعاء دالة calculation lambda وتخزين نتيجتها. أثناء إعادة التركيب، تعرض الدالة 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 إبطال صحة البيانات المخزّنة مؤقتًا وإعادة تنفيذ كتلة رمز lambda الخاصة بالحساب في المرة التالية التي تتم فيها إعادة إنشاء الوظيفة. تمنحك هذه الآلية إمكانية التحكّم في مدة بقاء أحد العناصر في 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) ) { /* ... */ } }
في المقتطف التالي، يتم نقل الحالة إلى فئة عادية لتخزين الحالة
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. يتم إبطال صحة ذاكرة التخزين المؤقت عند تغيير أي من المدخلات. في المرة التالية التي تتم فيها إعادة إنشاء الدالة، سيتم إعادة تنفيذ كتلة lambda الخاصة بالحساب 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