State وJetpack Compose

الحالة في التطبيق هي أي قيمة يمكن أن تتغيّر بمرور الوقت. هذا تعريف واسع جدًا ويشمل كل شيء من قاعدة بيانات الغرفة إلى متغير في فئة ما.

تعرض جميع تطبيقات 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 في المقطوعة الموسيقية خلال المقطوعة الموسيقية الأولية، ويتم عرض القيمة المخزنة أثناء إعادة التركيب. يمكن استخدام 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) }

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

تتطلب بنية تفويض 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.6.2")
}

رائع

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

    تكون collectAsState معادِلة collectAsStateWithLifecycle، لأنها تجمع أيضًا القيم من Flow وتحولها إلى إنشاء State.

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

    لا يلزم توفير اعتماديات إضافية لـ collectAsState، لأنها متوفرة في compose-runtime.

  • LiveData: observeAsState()

    يبدأ observeAsState() في ملاحظة LiveData هذا ويمثل قيمه من خلال State.

    الاعتمادية التالية مطلوبة في ملف build.gradle:

Kotlin

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

رائع

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

Kotlin

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

رائع

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

Kotlin

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

رائع

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

حالة تسجيل الدخول مقابل الحالة غير المُسجّلة

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

عنصر بدون حالة قابل للإنشاء هو عنصر قابل للإنشاء لا يحمل أي حالة. هناك طريقة سهلة لإلغاء الحالة هي استخدام رفع الدول.

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

الرفع إلى الولاية

إن رفع الحالة في Compose هو نمط من الانتقال إلى متصل ضِمن المحتوى القابل للإنشاء لجعله غير مرتبط بالحالة. النمط العام لرفع الحالة في Jetpack Compose هو استبدال متغير الحالة بمعلمتين:

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

ومع ذلك، فأنت غير مقيد بـ onValueChange. إذا كانت الأحداث الأكثر تحديدًا مناسبة للعنصر القابل للإنشاء، يجب عليك تحديدها باستخدام lambdas.

للولاية التي تم رفعها بهذه الطريقة بعض الخصائص المهمة:

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

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

أداة حفظ الخرائط

إذا كان @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 واستخدام فهارسها كمفاتيح:

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

تخزين حالة المفاتيح باستخدام مفاتيح تتجاوز إعادة التركيب

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

العيّنات

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

الفيديوهات الطويلة

المدوّنات