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.

للاطّلاع على مزيد من المعلومات عن عملية الإنشاء الأولي وإعادة الإنشاء، اطّلِع على مقالة التفكير في وضع "الإنشاء".

الحالة في عنصر قابل للإنشاء

يمكن للدوال القابلة للإنشاء استخدام واجهة برمجة تطبيقات remember لتخزين عنصر في الذاكرة. يتم تخزين القيمة التي يتم احتسابها بواسطة remember في التركيب أثناء التركيب الأولي، ويتم عرض القيمة المخزّنة أثناء إعادة التركيب. يمكن استخدام remember لتخزين كلّ من الكائنات القابلة للتغيير وغير القابلة للتغيير.

mutableStateOf ينشئ MutableState<T> نوعًا قابلاً للملاحظة ومتكاملاً مع وقت تشغيل إنشاء الرسالة.

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) }

هذه التعريفات متكافئة، ويتم تقديمها كطريقة سهلة لاستخدام بنية الجملة في استخدامات مختلفة لـ state. عليك اختيار الرمز الذي ينتج عن العبارة العبارة السهلة القراءة في العبارة المركبة التي تكتبها.

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

الأنواع الأخرى المتوافقة من الحالات

لا يتطلّب الإنشاء استخدام MutableState<T> للاحتفاظ بالحالة، فهو يتيح استخدام أنواع أخرى قابلة للملاحظة. قبل قراءة نوع آخر من أنواع العناصر القابلة للتتبّع في Compose، يجب تحويله إلى State<T> حتى تتمكّن العناصر القابلة للتجميع من إعادة التركيب تلقائيًا عند تغيُّر الحالة.

أنشئ سفنًا مع دوال لإنشاء State<T> من الأنواع الشائعة القابلة للملاحظة المستخدمة في تطبيقات Android. قبل استخدام عمليات الدمج هذه، أضِف الأدوات المناسبة كما هو موضّح أدناه:

  • Flow: collectAsStateWithLifecycle()

    يجمع collectAsStateWithLifecycle() القيم من Flow بطريقة الوعي بمراحل النشاط، ما يسمح لتطبيقك بالحفاظ على موارد التطبيق. ويمثّل أحدث قيمة تمّ بثّها من State. استخدِم واجهة برمجة التطبيقات هذه كطريقة مقترَحة لجمع بيانات مسارات الإحالات الناجحة على تطبيقات Android.

    يجب استخدام الملحق التالي في ملف build.gradle (يجب أن يكون الإصدار 2.6.0-beta01 أو إصدار أحدث):

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.5")
}

رائع

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

رائع

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

Kotlin

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

رائع

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

Kotlin

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

رائع

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

جدار الحماية المتتبِّع لحالة الاتصال مقابل جدار الحماية غير المتتبِّع لحالة الاتصال

إنّ العنصر القابل للتجميع الذي يستخدم 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 بشكلٍ متكرّر مع MutableState:

var name by remember { mutableStateOf("") }

في هذه الحالة، يؤدي استخدام الدالة remember إلى الاحتفاظ بقيمة MutableState في عمليات إعادة التركيب.

بشكل عام، تأخذ remember مَعلمة lambda calculation. عند تشغيل remember للمرة الأولى، يتمّ استدعاء دالة lambda calculation وتخزين نتيجتها. أثناء إعادة التركيب، تعرض remember القيمة التي تم تخزينها آخر مرة.

بالإضافة إلى حالة التخزين المؤقت، يمكنك أيضًا استخدام remember لتخزين أي عنصر أو نتيجة عملية في التركيب تتطلّب وقتًا طويلاً لبدء تشغيلها أو احتسابها. قد لا ترغب في تكرار هذه العملية الحسابية في كل عملية إعادة التركيب. على سبيل المثال، إنشاء عنصر ShaderBrush هذا، وهو عملية مكلفة:

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

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

نماذج

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

الفيديوهات

المدوّنات