State وJetpack Compose

الحالة في التطبيق هي أي قيمة يمكن أن تتغير بمرور الوقت. وهذا تعريف واسع جدًا يشمل كل شيء، بدءًا من قاعدة بيانات 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 بطريقة تراعي مراحل النشاط، ما يتيح لتطبيقك الحفاظ على موارد التطبيق. تمثّل هذه السمة آخر قيمة تم إصدارها من Compose State. استخدِم واجهة برمجة التطبيقات هذه كطريقة مقترَحة لجمع البيانات عن مسارات الإحالة الناجحة على تطبيقات 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"
}
  • Flow: collectAsState()

    تشبه الدالة collectAsState الدالة collectAsStateWithLifecycle، لأنّها تجمع أيضًا القيم من Flow وتحوّلها إلى State في Compose.

    استخدِم collectAsState للرمز البرمجي الذي لا يعتمد على نظام أساسي معيّن بدلاً من collectAsStateWithLifecycle، الذي يقتصر على Android.

    لا يلزم توفُّر تبعيات إضافية لاستخدام collectAsState، لأنّه متاح في compose-runtime.

  • LiveData: observeAsState()

    يبدأ 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"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.11.0")
}

أنيق

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.11.0"
}

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

نماذج

اختبارات الرموز

الفيديوهات

المدوّنات