الحالة في التطبيق هي أي قيمة يمكن أن تتغير بمرور الوقت. هذا تعريف واسع جدًا يشمل كل شيء، بدءًا من قاعدة بيانات 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في عنصر قابل للإنشاء مختلف، سيسمح لك النقل بذلك. - قابلة للاعتراض: يمكن للمتصلين بوظائف 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 للمرة الأولى، يتم استدعاء دالة 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