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.

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

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

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

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

  • Flow: collectAsStateWithLifecycle()

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

    يجب توفير التبعية التالية في ملف build.gradle (يجب أن تكون بالإصدار 2.6.0 إلى الإصدار التجريبي 2.6.0 أو إصدار أحدث):

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

رائع

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

Kotlin

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

رائع

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

Kotlin

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

رائع

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

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

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

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

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

العيّنات

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

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

المدوّنات