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

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

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

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

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

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

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

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

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

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

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

عناصر الاحتفاظ بالحالة ومسؤولياتها

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

ويؤدي ذلك إلى تحقيق المزايا التالية:

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

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

أنواع حاملي الشهادات

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

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

تلقي الأقسام التالية نظرة فاحصة على أنواع عناصر الاحتفاظ بالحالة، بدءًا بعنصر الاحتفاظ بالحالة الخاص بمنطق النشاط التجاري.

منطق النشاط التجاري وحامل حالته

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

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

على سبيل المثال، لنفترض أنّك تريد الانتقال إلى وجهة خاصة بالمؤلف في تطبيق Now in Android:

يوضّح تطبيق Now in Android كيف يجب أن يكون لوجهة التنقّل التي تمثّل إحدى وظائف التطبيق الرئيسية أداة خاصة بها للاحتفاظ بحالة منطق النشاط الفريد.
الشكل 4. تطبيق Now in 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 وبما أنّها يتم تنفيذها باستخدام ViewModel، سيتم الاحتفاظ بها عند إعادة إنشاء Activity سريعًا. في حال توقّف العملية، يمكن قراءة العنصر SavedStateHandle لتوفير الحد الأدنى من المعلومات المطلوبة لاستعادة حالة واجهة المستخدم من طبقة البيانات.
يحتوي على حالة طويلة الأمد يقتصر نطاق ViewModel على الرسم البياني للتنقّل، لذلك ما لم تتم إزالة وجهة المؤلف من الرسم البياني للتنقّل، ستظل حالة واجهة المستخدم في uiState StateFlow محفوظة في الذاكرة. يوفّر استخدام StateFlow أيضًا ميزة تأجيل تطبيق منطق النشاط التجاري الذي ينتج الحالة، لأنّه لا يتم إنتاج الحالة إلا إذا كان هناك جامع لحالة واجهة المستخدم.
فريد من نوعه في واجهة المستخدم لا ينطبق AuthorViewModel إلا على وجهة التنقّل الخاصة بالمؤلف ولا يمكن إعادة استخدامه في أي مكان آخر. إذا كانت هناك أي منطق نشاط تجاري تتم إعادة استخدامه في جميع وجهات التنقّل، يجب تغليف منطق النشاط التجاري هذا في مكوّن ذي نطاق على مستوى البيانات أو النطاق.

‫ViewModel كحاوية لحالة منطق النشاط التجاري

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

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

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

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

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

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

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

يستخدم تطبيق Now in Android عنصر احتفاظ بحالة فئة عادية لإدارة منطق واجهة المستخدم
الشكل 5. تطبيق Now in Android التجريبي

يعرض تطبيق Now in 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 التالية كيفية استخدام عناصر الاحتفاظ بالحالة في طبقة واجهة المستخدم. يمكنك استكشافها للاطّلاع على هذه الإرشادات عمليًا: