تصميم واجهة مستخدم Compose

في ميزة "الإنشاء"، تكون واجهة المستخدم ثابتة، ولا يمكن تعديلها بعد سحبها. ما يمكنك التحكّم فيه هو حالة واجهة المستخدم. في كل مرة تتغيّر فيها حالة واجهة المستخدم، يعيد تطبيق Compose إنشاء أجزاء شجرة واجهة المستخدم التي تغيّرت. يمكن للعناصر القابلة للتجميع قبول الحالة وعرض الأحداث. على سبيل المثال، يقبل TextField قيمة ويعرض مكالمة استدعاء onValueChange تطلب من معالِج المكالمة الاستدعاء تغيير القيمة.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

بما أنّ العناصر القابلة للتجميع تقبل الحالة وتُظهر الأحداث، فإنّ نموذج تدفق البيانات أحادي الاتجاه يناسب Jetpack Compose بشكلٍ جيد. يركز هذا الدليل على كيفية تنفيذ نمط تدفق البيانات أحادي الاتجاه في Compose، وكيفية تنفيذ الأحداث ومقدّمي الحالة، وكيفية العمل مع ViewModels في Compose.

تدفق البيانات أحادي الاتجاه

تدفق البيانات أحادي الاتجاه (UDF) هو نمط تصميم يتدفق فيه الحدث من الأعلى إلى الأسفل ويتدفق الحدث من الأسفل إلى الأعلى. من خلال اتّباع عملية تدفق البيانات أحادية الاتجاه، يمكنك فصل العناصر القابلة للتجميع التي تعرض الحالة في واجهة المستخدم عن أجزاء تطبيقك التي تخزِّن الحالة وتغيّرها.

تبدو حلقة تعديل واجهة المستخدم لتطبيق يستخدم تدفق البيانات أحادي الاتجاه على النحو التالي:

  1. الحدث: يُنشئ جزء من واجهة المستخدم حدثًا ويمرّره للأعلى، مثل النقر على زر يتم تمريره إلى ViewModel للمعالجة، أو يتم تمرير حدث من طبقات أخرى من تطبيقك، مثل الإشارة إلى أنّ جلسة المستخدم انتهت.
  2. تعديل الحالة: قد يغيّر معالِج الأحداث الحالة.
  3. عرض الحالة: ينقل حامل الحالة الحالة، ويعرضها واجهة المستخدم.

تتدفق الأحداث من واجهة المستخدم إلى عنصر الاحتفاظ بالحالة، وتتدفق الحالة من عنصر الاحتفاظ بالحالة إلى واجهة المستخدم.
الشكل 1. تدفق البيانات أحادي الاتجاه:

يوفّر اتّباع هذا النمط عند استخدام Jetpack Compose العديد من المزايا:

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

تدفق البيانات أحادي الاتجاه في Jetpack Compose

تعمل العناصر القابلة للتجميع استنادًا إلى الحالة والأحداث. على سبيل المثال، لا يتم تعديل TextField إلا عند تعديل مَعلمة value وعرض onValueChange مكالمة استدعاء، وهو حدث يطلب تغيير القيمة إلى قيمة جديدة. تحدِّد دالة Compose كائن State كحامل قيمة، وتؤدي التغييرات في قيمة الحالة إلى إعادة التركيب. يمكنك الاحتفاظ بالحالة في remember { mutableStateOf(value) } أو rememberSaveable { mutableStateOf(value)، وذلك حسب المدة التي تحتاج فيها إلى تذكُّر القيمة.

نوع قيمة العنصر القابل للتجميع TextField هو String، لذا يمكن أن تأتي هذه القيمة من أي مكان، سواء من قيمة مُبرمَجة بشكلٍ ثابت أو من ViewModel أو من العنصر القابل للتجميع الرئيسي. لست مضطرًا إلى الاحتفاظ بها في عنصر State، ولكن عليك تعديل القيمة عند استدعاء onValueChange.

تحديد المَعلمات القابلة للتجميع

عند تحديد مَعلمات الحالة لعنصر قابل للتجميع، يجب مراعاة الأسئلة التالية:

  • ما مدى قابلية إعادة استخدام العنصر القابل للتجميع أو مرونته؟
  • كيف تؤثر مَعلمات الحالة في أداء هذا المكوّن القابل للتجميع؟

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

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

في بعض الأحيان، يؤدي استخدام المَعلمات الفردية أيضًا إلى تحسين الأداء. على سبيل المثال، إذا كان News يحتوي على معلومات أكثر من title وsubtitle فقط، عند تمرير مثيل جديد من News إلى Header(news)، سيتم مجددًا تجميع العنصر القابل للتجميع، حتى إذا لم تتغيّر title وsubtitle.

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

الأحداث في ميزة "الإنشاء"

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

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

يُفضَّل تمرير قيم ثابتة لواجهات دالة lambda الخاصة بالحالة ومعالج الأحداث. ينطوي هذا الأسلوب على المزايا التالية:

  • تحسين إمكانية إعادة الاستخدام
  • التأكّد من أنّ واجهة المستخدم لا تغيّر قيمة الحالة مباشرةً
  • يمكنك تجنُّب مشاكل المعالجة المتزامنة لأنّك تتأكّد من عدم تحوُّل الحالة من سلسلة محادثات أخرى.
  • وغالبًا ما يؤدي ذلك إلى تقليل تعقيد الرمز البرمجي.

على سبيل المثال، يمكن استدعاء عنصر قابل للتجميع يقبل String ولامدا كمَعلمات من العديد من السياقات، وهو قابل لإعادة الاستخدام بدرجة كبيرة. لنفترض أنّه يتم عرض نص دائمًا في شريط التطبيق العلوي في تطبيقك، وأنّه يتضمّن زر الرجوع. يمكنك تحديد MyAppTopAppBar تركيبة أكثر عمومية تتلقّى النص واسم ملف التمرير للخلف كمَعلمات:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

نماذج العرض والحالات والأحداث: مثال

باستخدام ViewModel وmutableStateOf، يمكنك أيضًا توفير تدفق بيانات أحادي الاتجاه في تطبيقك في حال استيفاء أحد الشروط التالية:

  • يتم عرض حالة واجهة المستخدم من خلال حوامل الحالة القابلة للتتبّع، مثل StateFlow أو LiveData.
  • يعالج ViewModel الأحداث الواردة من واجهة المستخدم أو الطبقات الأخرى من تطبيقك ويحدّث حامل الحالة استنادًا إلى الأحداث.

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

تتضمّن الشاشة أربع حالات:

  • تم تسجيل الخروج: عندما لم يسجّل المستخدم الدخول بعد
  • جارٍ: عندما يحاول تطبيقك حاليًا تسجيل دخول المستخدم من خلال إجراء طلب بيانات من الشبكة.
  • خطأ: عندما يحدث خطأ أثناء تسجيل الدخول.
  • مسجّل الدخول: عندما يكون المستخدم مسجّلاً الدخول.

يمكنك وضع نماذج لهذه الحالات على أنّها فئة مختومة. يعرِض ViewModel الحالة على أنّه State، ويضبط الحالة الأولية، ويحدّث الحالة حسب الحاجة. يعالج العنصر ViewModel أيضًا حدث تسجيل الدخول من خلال عرض طريقة onSignIn().

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

بالإضافة إلى واجهة برمجة التطبيقات mutableStateOf، توفّر أداة Compose LiveData وFlow و Observable لتسجيلها كمستمع وتمثيل القيمة كحالة.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

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

لمزيد من المعلومات حول البنية في Jetpack Compose، يمكنك الرجوع إلى المراجع التالية:

نماذج