أصحاب الحالة وحالة واجهة المستخدم

يناقش دليل طبقة واجهة المستخدم تدفق البيانات أحادي الاتجاه (UDF) كوسيلة لإنتاج وإدارة حالة واجهة المستخدم لطبقة واجهة المستخدم.

تتدفق البيانات في اتجاه واحد من طبقة البيانات إلى واجهة المستخدم.
الشكل 1: تدفق البيانات أحادي الاتجاه

كما أنه يسلط الضوء على فوائد تفويض إدارة UDF إلى فئة خاصة تسمى حامل الدولة. ويمكنك تنفيذ عنصر حفظ الحالة إما من خلال ViewModel أو فئة عادية. يلقي هذا المستند نظرة فاحصة على أصحاب الدول والدور الذي يلعبونه في طبقة واجهة المستخدم.

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

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

عناصر مسار إنتاج حالة واجهة المستخدم

تُحدِّد حالة واجهة المستخدم والمنطق الذي يتسبب بها طبقة واجهة المستخدم.

حالة واجهة المستخدم

حالة واجهة المستخدم هي الخاصية التي تصف واجهة المستخدم. هناك نوعان من حالات واجهة المستخدم:

  • حالة واجهة مستخدم الشاشة هي ما تحتاج إلى عرضه على الشاشة. على سبيل المثال، يمكن أن يحتوي صف NewsUiState على المقالات الإخبارية والمعلومات الأخرى اللازمة لعرض واجهة المستخدم. عادةً ما تكون هذه الحالة مرتبطة بطبقات أخرى من التسلسل الهرمي لأنها تحتوي على بيانات التطبيق.
  • تشير حالة عنصر واجهة المستخدم إلى الخصائص الأساسية لعناصر واجهة المستخدم التي تؤثر في طريقة عرضها. قد يظهر عنصر واجهة المستخدم أو مخفيًا وقد يحتوي على خط أو حجم خط أو لون خط معين. في الملفات الشخصية على Android، تدير طريقة العرض هذه الحالة نفسها حيث إنها بطبيعتها تعرض طرقًا لتعديل حالتها أو الاستعلام عنها. ومن الأمثلة على ذلك طريقتَي get و set للفئة TextView للنص الخاص بها. في Jetpack Compose، تكون الحالة خارجية عن المادة القابلة للتعديل، ويمكنك حتى رفعها بعيدًا عن القرب القريب من المادة المركبة إلى دالة الاتصال أو حامل الحالة. ومن الأمثلة على ذلك ScaffoldState في مادة عرض Scaffold.

ألعاب المنطق

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

المنطق ينتج حالة واجهة المستخدم
الشكل 2: المنطق كمنتج لحالة واجهة المستخدم

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

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

مراحل نشاط Android وأنواع حالة ومنطق واجهة المستخدم

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

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

يمكن تلخيص ما سبق بالجدول التالي:

مستقلة عن دورة حياة واجهة المستخدم يعتمد على مراحل نشاط واجهة المستخدم
منطق العمل منطق واجهة المستخدم
حالة واجهة مستخدم الشاشة

مسار إنتاج حالة واجهة المستخدم

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

أي أن التبديلات التالية لمسار طبقة واجهة المستخدم صالحة:

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

    @Composable
    fun Counter() {
        // The UI state is managed by the UI itself
        var count by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { ++count }) {
                Text(text = "Increment")
            }
            Button(onClick = { --count }) {
                Text(text = "Decrement")
            }
        }
    }
    
  • منطق واجهة المستخدم ← واجهة المستخدم على سبيل المثال، إظهار أو إخفاء زر يسمح للمستخدم بالانتقال إلى أعلى القائمة.

    @Composable
    fun ContactsList(contacts: List<Contact>) {
        val listState = rememberLazyListState()
        val isAtTopOfList by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex < 3
            }
        }
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Show or hide the button (UI logic) based on the list scroll position
        AnimatedVisibility(visible = !isAtTopOfList) {
            ScrollToTopButton()
        }
    }
    
  • منطق الأعمال ← واجهة المستخدم عنصر في واجهة المستخدم يعرض صورة المستخدم الحالي على الشاشة.

    @Composable
    fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
        // Call on the UserAvatar Composable to display the photo
        UserAvatar(picture = uiState.profilePicture)
    }
    
  • منطق الأعمال ← منطق واجهة المستخدم ← واجهة المستخدم عنصر في واجهة المستخدم يتم تمريره لعرض المعلومات الصحيحة على الشاشة لحالة واجهة مستخدم معينة.

    @Composable
    fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        val contacts = uiState.contacts
        val deepLinkedContact = uiState.deepLinkedContact
    
        val listState = rememberLazyListState()
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Perform UI logic that depends on information from business logic
        if (deepLinkedContact != null && contacts.isNotEmpty()) {
            LaunchedEffect(listState, deepLinkedContact, contacts) {
                val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
                if (deepLinkedContactIndex >= 0) {
                  // Scroll to deep linked item
                  listState.animateScrollToItem(deepLinkedContactIndex)
                }
            }
        }
    }
    

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

تدفق البيانات من طبقة إنتاج البيانات إلى واجهة المستخدم
الشكل 3: تطبيق المنطق في طبقة واجهة المستخدم

أصحاب الدولة ومسؤولياتهم

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

وينتج عن ذلك الفوائد التالية:

  • واجهات مستخدم بسيطة: تربط واجهة المستخدم حالتها فقط.
  • قابلية الصيانة: يمكن تكرار المنطق المحدد في مالك الحالة بدون تغيير واجهة المستخدم نفسها.
  • قابلية الاختبار: يمكن اختبار واجهة المستخدم ومنطق الإنتاج في حالتها بشكل مستقل.
  • سهولة القراءة: يمكن لقرّاء الرمز رؤية الاختلافات بين رمز العرض التقديمي لواجهة المستخدم ورمز إنتاج حالة واجهة المستخدم بوضوح.

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

أنواع الجهات المانحة في الدولة

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

  • صاحب حالة منطق العمل.
  • جهة الاحتفاظ بحالة منطق واجهة المستخدم.

تُلقي الأقسام التالية نظرة عن كثب على أنواع أصحاب الدول، بدءًا من صاحب حالة منطق العمل.

منطق العمل والدولة

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

الخاصية التفاصيل
عرض حالة واجهة المستخدم يتحمل أصحاب حالة منطق العمل مسؤولية إنتاج حالة واجهة المستخدم لواجهات المستخدم الخاصة بهم. غالبًا ما تنتج حالة واجهة المستخدم هذه عن معالجة أحداث المستخدم وقراءة البيانات من النطاق وطبقات البيانات.
الاحتفاظ بالجمهور من خلال ترفيه النشاط يحتفظ أصحاب حالة منطق النشاط التجاري بمسارات المعالجة على مستوى الولاية والولاية في إعادة تشغيل Activity، ما يساعد في توفير تجربة سلسة للمستخدم. في الحالات التي يتعذّر فيها الاحتفاظ ببيانات المستخدم وتتم إعادة إنشائه (عادةً بعد انتهاء العملية)، يجب أن يكون مالك الحالة قادرًا على إعادة إنشاء حالته الأخيرة بسهولة لضمان تقديم تجربة مستخدِم متّسقة.
امتلاك حالة طويلة الأمد غالبًا ما يتم استخدام أصحاب حالة منطق العمل لإدارة الحالة لوجهات التنقّل. ونتيجة لذلك، فإنها غالبًا ما تحافظ على حالتها عبر تغييرات التنقل حتى تتم إزالتها من الرسم البياني للتنقل.
فريدة لواجهة المستخدم ولا يمكن إعادة استخدامها ينتج عادةً مالكو حالة منطق العمل هذه الحالة لوظيفة معيّنة في التطبيق، على سبيل المثال TaskEditViewModel أو TaskListViewModel، وبالتالي لا تسري إلا على وظيفة التطبيق هذه. يمكن لمالك الحالة نفسه إتاحة وظائف التطبيق هذه من خلال أشكال الأجهزة المختلفة. على سبيل المثال، قد تعيد إصدارات التطبيق المتوافقة مع الأجهزة الجوّالة والتلفزيون والأجهزة اللوحية إعادة استخدام عنصر حالة منطق العمل نفسه.

على سبيل المثال، انظر إلى وجهة تنقل المؤلف في تطبيق "الآن في Android":

يوضح تطبيق Now في Android كيف يجب أن يكون لوجهة التنقل التي تمثل وظيفة رئيسية في التطبيق
صاحب حالة فريدة لمنطق الأعمال.
الشكل 4: تطبيق "الآن في Android"

وبصفتها مالك حالة منطق العمل، تعرض AuthorViewModel حالة واجهة المستخدم في هذه الحالة:

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = …

    // Business logic
    fun followAuthor(followed: Boolean) {
      …
    }
}

لاحِظ أنّ السمة AuthorViewModel تتضمّن السمات الموضّحة سابقًا:

الخاصية التفاصيل
ينتج AuthorScreenUiState تقرأ السمة AuthorViewModel البيانات من AuthorsRepository وNewsRepository وتستخدم هذه البيانات لإنتاج AuthorScreenUiState. ويتم أيضًا تطبيق منطق العمل عندما يريد المستخدم متابعة Author أو إلغاء متابعته من خلال تفويض المستخدمين إلى AuthorsRepository.
يمكنها الوصول إلى طبقة البيانات يتم تمرير مثيل AuthorsRepository وNewsRepository إليه في الدالة الإنشائية الخاصة به، ما يسمح بتنفيذ منطق العمل المتَّبَع لـ Author.
البقاء على قيد الحياة في لعبة ترفيهية واحدة (Activity) وسيتم الاحتفاظ بها في تجربة Activity سريعة، وذلك لأنّه يتم تنفيذها باستخدام ViewModel. في حال إيقاف العملية، يمكن قراءة العنصر SavedStateHandle لتوفير الحدّ الأدنى من المعلومات المطلوبة لاستعادة حالة واجهة المستخدم من طبقة البيانات.
يمتلك حالة طويلة الأمد يتم تحديد نطاق ViewModel على الرسم البياني للتنقّل، ولذلك ما لم تتم إزالة وجهة المؤلف من الرسم البياني للتنقل، ستظل حالة واجهة المستخدم في StateFlow uiState في الذاكرة. ويضيف أيضًا استخدام StateFlow ميزة تطبيق منطق العمل الذي ينتج عنه كسول الحالة لأنّ الحالة يتم إنتاجها فقط إذا توفّر جامع لحالة واجهة المستخدم.
فريد لواجهة المستخدم الخاصة به لا ينطبق AuthorViewModel إلا على وجهة تنقل المؤلف، ولا يمكن إعادة استخدامه في أي مكان آخر. إذا كان هناك أي منطق عمل مُعاد استخدامه في وجهات التنقّل، يجب تضمين منطق العمل هذا في مكوِّن على مستوى طبقة البيانات أو النطاق.

ViewModel كصاحب حالة منطق العمل

إنّ فوائد ViewModels في تطوير Android تجعلها مناسبة لإتاحة الوصول إلى منطق العمل وإعداد بيانات التطبيق لعرضها على الشاشة. وتشمل هذه المزايا ما يلي:

  • تبقى العمليات التي تُشغّلها ViewModels ظاهرةً في تغييرات الإعدادات.
  • التكامل مع ميزة التنقل:
    • يخزن التنقل مؤقتًا ViewModels أثناء وجود الشاشة في الحزمة الخلفية. من المهم أن تكون البيانات التي تم تحميلها سابقًا متاحة على الفور عند عودتك إلى وجهتك. يعد ذلك شيئًا أكثر صعوبة في القيام به مع صاحب ولاية يتبع دورة حياة الشاشة القابلة للإنشاء.
    • يتم أيضًا محو ViewModel عند ظهور الوجهة من المكدس الخلفي، ما يضمن تنظيف الحالة تلقائيًا. يختلف ذلك عن الاستماع إلى التخلص من الجهاز والتي يمكن أن تحدث لأسباب متعددة، مثل الانتقال إلى شاشة جديدة أو تغيير في الإعدادات أو لأسباب أخرى.
  • التكامل مع مكتبات Jetpack الأخرى، مثل Hilt

منطق واجهة المستخدم وحالته

منطق واجهة المستخدم هو منطق يعمل على البيانات التي توفرها واجهة المستخدم نفسها. وقد يكون ذلك بشأن حالة عناصر واجهة المستخدم أو في مصادر بيانات واجهة المستخدم مثل واجهة برمجة التطبيقات للأذونات أو Resources. عادةً ما يكون لمالكي الولايات الذين يستخدمون منطق واجهة المستخدم السمات التالية:

  • عرض حالة واجهة المستخدم وإدارة حالة عناصر واجهة المستخدم.
  • لا تنجو من إعادة إنشاء Activity: غالبًا ما يعتمد أصحاب الحالات الذين تتم استضافتهم في منطق واجهة المستخدم على مصادر البيانات من واجهة المستخدم نفسها، ويحاولون الاحتفاظ بهذه المعلومات عبر تغييرات الإعدادات في أغلب الأحيان أكثر من عدم التسبب في تسرب الذاكرة. إذا احتاج أصحاب الحالات إلى الاحتفاظ بالبيانات حتى يستمر تغيير الإعدادات، عليهم تفويض المستخدمين إلى مكوّن آخر أكثر ملاءمةً لنجاح عملية Activity. في Jetpack Compose على سبيل المثال، حالات عناصر واجهة المستخدم القابلة للإنشاء التي يتم إنشاؤها باستخدام دوال remembered يتم تفويضها غالبًا إلى rememberSaveable للحفاظ على الحالة في إنشاء "Activity". ومن أمثلة هذه الدوال rememberScaffoldState() وrememberLazyListState().
  • تتضمّن إشارات إلى مصادر البيانات على مستوى واجهة المستخدم: يمكن الإشارة بأمان إلى مصادر البيانات، مثل واجهات برمجة التطبيقات والموارد الخاصة بدورة الحياة، وقراءتها لأنّ مالك حالة واجهة المستخدم له دورة حياة واجهة المستخدم نفسها.
  • قابلة لإعادة الاستخدام في واجهات مستخدم متعددة: قد تتم إعادة استخدام مثيلات مختلفة لمالك حالة منطق واجهة المستخدم نفسه في أجزاء مختلفة من التطبيق. على سبيل المثال، يمكن استخدام حالة لإدارة أحداث إدخال المستخدم لمجموعة شرائح في صفحة بحث عن شرائح الفلتر، وأيضًا في حقل "إلى" لمستلمي الرسالة الإلكترونية.

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

ويمكن توضيح ما سبق في المثال التالي في نموذج Now في Android:

يستخدم Android الآن حامل حالة من الفئة العادية لإدارة منطق واجهة المستخدم
الشكل 5: نموذج تطبيق Now في Android

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

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

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

في المثال أعلاه، يمكن ملاحظة التفاصيل التالية بشأن NiaAppState:

  • لا تنجو من إنشاء Activity: إنّ السمة NiaAppState هي remembered في المقطوعة الموسيقية من خلال إنشائها باستخدام دالة قابلة للإنشاء rememberNiaAppState من خلال اتّباع اصطلاحات تسمية Compose. بعد إعادة إنشاء Activity، يتم فقدان المثيل السابق وإنشاء مثيل جديد مع تمرير جميع تبعياته، وذلك بشكل يتناسب مع الإعدادات الجديدة لـ Activity المعاد إنشاؤه. قد تكون هذه التبعيات جديدة أو تمت استعادتها من الإعدادات السابقة. على سبيل المثال، يتم استخدام rememberNavController() في الدالة الإنشائية NiaAppState وتفويضها إلى rememberSaveable للحفاظ على الحالة في Activity.
  • تتضمن إشارات إلى مصادر البيانات على مستوى واجهة المستخدم: يمكن الاحتفاظ بأمان بالإشارات إلى navigationController وResources وأنواع أخرى مشابهة على مستوى مراحل النشاط في NiaAppState لأنّهم يتشاركون في نطاق مراحل النشاط نفسه.

الاختيار بين ViewModel والفئة العادية لمالك الحالة

من الأقسام أعلاه، يعتمد الاختيار بين ViewModel وحامل حالة من الفئة العادية على المنطق المطبّق على حالة واجهة المستخدم ومصادر البيانات التي يعمل عليها المنطق.

باختصار، يُظهر المخطّط أدناه موضع أصحاب الدولة في مسار الإنتاج في واجهة المستخدم:

تدفق البيانات من طبقة إنتاج البيانات إلى طبقة واجهة المستخدم
الشكل 6: الجهات التابعة للولاية في مسار إنتاج حالة واجهة المستخدم. تشير الأسهم إلى تدفق البيانات.

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

يمكن إسناد الطلبات إلى أصحاب الدولة

يمكن أن يعتمد أصحاب الدولة على أصحاب الدولة الآخرين ما دامت التبعيات لها عمر مساوي أو أقصر. ومن الأمثلة على ذلك:

  • يمكن أن يعتمد صاحب حالة منطق واجهة المستخدم على صاحب حالة منطق واجهة المستخدم الآخر.
  • قد يعتمد صاحب الحالة على مستوى الشاشة على صاحب الحالة المنطقية لواجهة المستخدم.

يوضِّح مقتطف الرمز التالي كيف يعتمد DrawerState في Compose على صاحب حالة داخلي آخر، وهو SwipeableState، وكيف يمكن أن يعتمد مالك حالة منطق واجهة المستخدم على DrawerState:

@Stable
class DrawerState(/* ... */) {
  internal val swipeableState = SwipeableState(/* ... */)
  // ...
}

@Stable
class MyAppState(
  private val drawerState: DrawerState,
  private val navController: NavHostController
) { /* ... */ }

@Composable
fun rememberMyAppState(
  drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
  navController: NavHostController = rememberNavController()
): MyAppState = remember(drawerState, navController) {
  MyAppState(drawerState, navController)
}

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

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

class MyScreenViewModel(/* ... */) {
  val uiState: StateFlow<MyScreenUiState> = /* ... */
  fun doSomething() { /* ... */ }
  fun doAnotherThing() { /* ... */ }
  // ...
}

@Stable
class MyScreenState(
  // DO NOT pass a ViewModel instance to a plain state holder class
  // private val viewModel: MyScreenViewModel,

  // Instead, pass only what it needs as a dependency
  private val someState: StateFlow<SomeState>,
  private val doSomething: () -> Unit,

  // Other UI-scoped types
  private val scaffoldState: ScaffoldState
) {
  /* ... */
}

@Composable
fun rememberMyScreenState(
  someState: StateFlow<SomeState>,
  doSomething: () -> Unit,
  scaffoldState: ScaffoldState = rememberScaffoldState()
): MyScreenState = remember(someState, doSomething, scaffoldState) {
  MyScreenState(someState, doSomething, scaffoldState)
}

@Composable
fun MyScreen(
  modifier: Modifier = Modifier,
  viewModel: MyScreenViewModel = viewModel(),
  state: MyScreenState = rememberMyScreenState(
    someState = viewModel.uiState.map { it.toSomeState() },
    doSomething = viewModel::doSomething
  ),
  // ...
) {
  /* ... */
}

يمثِّل المخطّط التالي التبعيات بين واجهة المستخدِم وأصحاب الحالات المختلفة لمقتطف الرمز السابق:

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

عيّنات

توضح نماذج Google التالية استخدام حاملي الحالات في طبقة واجهة المستخدم. يمكنك الانتقال إلى هذه الصفحة للاطّلاع على هذه الإرشادات عمليًا: