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

في 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.

  • تنشئ دالة mutableStateOf(value) كائن MutableState، وهو نوع قابل للتتبّع في Compose. وأي تغييرات في قيمتها تؤدي إلى إعادة جدولة أي دوال مركّبة تقرأ هذه القيمة.
  • تخزِّن الدالة remember العناصر في التركيب، وتتجاهل العنصر عندما تتم إزالة الدالة المركّبة التي استدعت remember من التركيب.
  • تحتفظ دالة rememberSaveable بالحالة في جميع تغييرات الضبط من خلال حفظها في Bundle.

تحديد مَعلمات الدوال المركّبة

عند تحديد مَعلمات الحالة لدالة مركّبة، ضَع الأسئلة التالية في الاعتبار:

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

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

@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.

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

الأحداث في Compose

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

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

يُفضَّل تمرير قيم غير قابلة للتغيير لمعالِجات الحالة والأحداث. يوفّر هذا الأسلوب المزايا التالية:

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

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

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

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

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

يمكنك تصميم هذه الحالات كفئة مختومة. يعرض 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، يُرجى الاطّلاع على المراجع التالية:

نماذج