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

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
  • 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.8.1")
}

Groovy

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

تتبُّع حالة الاتصال مقابل عدم تتبُّعها

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

نماذج

الدروس التطبيقية حول الترميز

الفيديوهات

المدوّنات