State وJetpack Compose

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

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.10.5")
}

أنيق

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.10.5"
}

Kotlin

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

أنيق

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

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.10.5")
}

أنيق

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.10.5"
}

العناصر المركّبة التي تحتفظ بالحالة مقابل العناصر المركّبة التي لا تحتفظ بالحالة

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

توضّح الأمثلة التالية طريقة عمل هذه الآلية.

في هذا المقتطف، يتم إنشاء ShaderBrush واستخدامه كخلفية طلاء لعنصر Box المركّب. remember تخزّن مثيل ShaderBrush لأنّ إعادة إنشائه مكلفة، كما سبق أن أوضحنا. تأخذ remember السمة avatarRes كمَعلمة key1، وهي صورة الخلفية المحدّدة. إذا تغيّرت avatarRes، يعيد الفرشاة إنشاء عنصر Composition باستخدام الصورة الجديدة، ويعيد تطبيقها على 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. يتم إبطال ذاكرة التخزين المؤقت عند تغيُّر أي من المدخلات. في المرة التالية التي يعيد فيها العنصر المركّب إنشاء عنصر Composition، تعيد rememberSaveable تنفيذ كتلة لامدا الخاصة بالحساب.

في المثال التالي، تخزّن rememberSaveable السمة userTypedQuery إلى أن تتغيّر typedQuery:

var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

مزيد من المعلومات

لمزيد من المعلومات عن الحالة وJetpack Compose، يُرجى الاطّلاع على المراجع الإضافية التالية.

نماذج

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

الفيديوهات

المدوّنات