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

أنيق

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.11.0"
}
  • ‫Flow: collectAsState()

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

    استخدِم collectAsState للرمز البرمجي غير المرتبط بنظام أساسي بدلاً من collectAsStateWithLifecycle المخصّص لنظام Android فقط.

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

  • ‫LiveData: observeAsState()

    يبدأ observeAsState() بمراقبة LiveData ويمثّل قيمه باستخدام State.

    يجب توفّر التبعية التالية في ملف build.gradle:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.12.1")
}

أنيق

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

Kotlin

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

أنيق

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

Kotlin

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

أنيق

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

الاحتفاظ بالحالة مقابل عدم الاحتفاظ بالحالة

تنشئ الدالة المركّبة التي تستخدم remember لتخزين عنصر حالة داخلية، ما يجعل الدالة المركّبة ذات حالة. ‫HelloContent هي مثال على دالة مركّبة تحتفظ بالحالة لأنّها تحتفظ بحالة name وتعدّلها داخليًا. يمكن أن يكون ذلك مفيدًا في الحالات التي لا يحتاج فيها المتصل إلى التحكّم في الحالة ويمكنه استخدامها بدون الحاجة إلى إدارة الحالة بنفسه. ومع ذلك، فإن الدوال المركّبة ذات الحالة الداخلية تكون أقل قابلية لإعادة الاستخدام وأكثر صعوبة في الاختبار.

الدالة المركّبة عديمة الحالة هي دالة مركّبة لا تحتفظ بأي حالة. من الطرق الشائعة لتحقيق ذلك استخدام نقل القيمة.

عند تطوير دوال مركّبة قابلة لإعادة الاستخدام، قد تحتاج غالبًا إلى عرض نسخة من الدالة المركّبة نفسها تتضمّن حالة وأخرى لا تتضمّن حالة. تُعدّ النسخة التي تتضمّن حالة مناسبة للمتصلين الذين لا يهتمون بالحالة، والنسخة التي لا تتضمّن حالة ضرورية للمتصلين الذين يحتاجون إلى التحكّم في الحالة أو رفعها.

نقل القيمة

"نقل القيمة" في Compose هو نمط لنقل الحالة إلى المتصل بدالة مركّبة لجعل الدالة المركّبة بدون حالة. النمط العام لنقل القيمة في Jetpack Compose هو استبدال متغيّر الحالة بمعلمتَين:

  • value: T: القيمة الحالية المطلوب عرضها
  • ‫onValueChange: (T) -> Unit: حدث يطلب تغيير القيمة، حيث T هي القيمة الجديدة المقترَحة

ومع ذلك، لا تقتصر على onValueChange. إذا كانت الأحداث الأكثر تحديدًا مناسبة للدالة المركّبة، يجب تحديدها باستخدام دوال lambda.

تتضمّن الحالة التي يتم رفعها بهذه الطريقة بعض السمات المهمة:

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

  1. يجب رفع الحالة إلى أدنى مستوى مشترك لجميع الدوال المركّبة التي تستخدم الحالة (قراءة).
  2. يجب رفع الحالة إلى أعلى مستوى يمكن تغييره (الكتابة).
  3. إذا تغيّرت حالتان استجابةً للأحداث نفسها، يجب رفعهما معًا.

يمكنك رفع الحالة إلى مستوى أعلى مما تتطلبه هذه القواعد، ولكن رفع الحالة إلى مستوى أدنى يجعل من الصعب أو المستحيل اتباع تدفق البيانات أحادي الاتجاه.

لمزيد من المعلومات، يُرجى الاطّلاع على صفحة أماكن رفع الحالة.

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

نماذج

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

الفيديوهات

المدوّنات