مكان الرفع

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

أفضل ممارسة

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

ويمكن أن يكون الأصل المشترك الأصغر خارج "مقطوعة موسيقية". على سبيل المثال، عند الرفع في ViewModel لأن منطق الأعمال متضمن.

تشرح هذه الصفحة أفضل الممارسات هذه بالتفصيل مع تنبيه يجب أخذه في الاعتبار.

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

توجد أدناه تعريفات لأنواع حالة ومنطق واجهة المستخدم يتم استخدامها في جميع أنحاء هذا المستند.

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

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

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

ألعاب المنطق

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

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

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

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

وفي ما يلي وصف لكلا الحلين وشرحًا متى يمكن استخدامهما.

عناصر قابلة للتعديل بصفتك مالك ولاية

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

ليست هناك حاجة إلى إجراء النقل بشكل رسمي

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

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails } // Apply simple UI logic
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

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

التحميل ضمن العناصر القابلة للإنشاء

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

المثال التالي هو تطبيق دردشة ينفِّذ عنصرَين من الوظائف:

  • يؤدي الزر "JumpToBottom" إلى تمرير قائمة الرسائل إلى الأسفل. يؤدي الزر إلى تنفيذ منطق واجهة المستخدم على حالة القائمة.
  • يتم تمرير قائمة MessagesList إلى الأسفل بعد أن يرسل المستخدم رسائل جديدة. يُنفِّذ UserInput منطق واجهة المستخدم في حالة القائمة.
في تطبيق Chat، اضغط على زرّ JumpToBottom ومرِّر سريعًا للأسفل في الرسائل الجديدة.
الشكل 1. تطبيق Chat مع زر "JumpToBottom" والانتقال للأسفل للوصول إلى الرسائل الجديدة

في ما يلي التسلسل الهرمي القابل للإنشاء:

الشجرة القابلة للإنشاء في Chat
الشكل 2. شجرة Chat القابلة للإنشاء

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

رفع حالة العمود الكسول من العمود LazyColumn إلى شاشة المحادثة
الشكل 3. جارٍ رفع حالة LazyColumn من LazyColumn إلى ConversationScreen

وأخيرًا، العناصر القابلة للإنشاء هي:

شجرة قابلة للإنشاء في Chat مع إضافة LazyListState إلى شاشة المحادثة
الشكل 4. تمت زيادة شجرة Chat القابلة للإنشاء مع "LazyListState" إلى "ConversationScreen"

الرمز على النحو التالي:

@Composable
private fun ConversationScreen(/*...*/) {
    val scope = rememberCoroutineScope()

    val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen

    MessagesList(messages, lazyListState) // Reuse same state in MessageList

    UserInput(
        onMessageSent = { // Apply UI logic to lazyListState
            scope.launch {
                lazyListState.scrollToItem(0)
            }
        },
    )
}

@Composable
private fun MessagesList(
    messages: List<Message>,
    lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value
) {

    LazyColumn(
        state = lazyListState // Pass hoisted state to LazyColumn
    ) {
        items(messages, key = { message -> message.id }) { item ->
            Message(/*...*/)
        }
    }

    val scope = rememberCoroutineScope()

    JumpToBottom(onClicked = {
        scope.launch {
            lazyListState.scrollToItem(0) // UI logic being applied to lazyListState
        }
    })
}

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

يُرجى العِلم أنّه يتم تحديد lazyListState في الطريقة MessagesList باستخدام القيمة التلقائية rememberLazyListState(). هذا نمط شائع في Compose. فهو يجعل العناصر القابلة للإنشاء أكثر مرونة وقابلية لإعادة الاستخدام. يمكنك بعد ذلك استخدام العنصر القابل للإنشاء في أجزاء مختلفة من التطبيق قد لا تحتاج إلى التحكم في الحالة. هذا هو الحال عادةً أثناء اختبار أو معاينة عنصر قابل للإنشاء. هذه هي بالضبط الطريقة التي يحدّد بها LazyColumn حالته.

الأصل المشترك الأدنى لـ LazyListState هو ConversationScreen.
الشكل 5. أقدم أصل مشترك لـ LazyListState هو ConversationScreen

فئة بسيطة كمالك للولاية

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

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

ويتم إنشاء هذه الفئات العادية وتذكُّرها في المقطوعة الموسيقية. وبما أنّها تتّبع دورة حياة العنصر القابل للإنشاء، يمكن أن تأخذ الأنواع التي توفّرها مكتبة Compose مثل rememberNavController() أو rememberLazyListState().

ومن الأمثلة على ذلك فئة مالك الحالة العادية LazyListState، التي تم تنفيذها في Compose للتحكُّم في مدى تعقيد واجهة المستخدم في LazyColumn أو LazyRow.

// LazyListState.kt

@Stable
class LazyListState constructor(
    firstVisibleItemIndex: Int = 0,
    firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
    /**
     *   The holder class for the current scroll position.
     */
    private val scrollPosition = LazyListScrollPosition(
        firstVisibleItemIndex, firstVisibleItemScrollOffset
    )

    suspend fun scrollToItem(/*...*/) { /*...*/ }

    override suspend fun scroll() { /*...*/ }

    suspend fun animateScrollToItem() { /*...*/ }
}

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

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

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

منطق الأعمال

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

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

ViewModels بصفتك مالك الدولة

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

عند رفع حالة واجهة المستخدم في ViewModel، يتم نقلها خارج "مقطوعة موسيقية".

يتم تخزين الحالة المسجَّلة على ViewModel خارج &quot;مقطوعة موسيقية&quot;.
الشكل 6. يتم تخزين الحالة التي تم رفعها إلى ViewModel خارج "مقطوعة موسيقية".

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

بعد ذلك، يكون ViewModel هو مصدر الحقيقة والأصل المشترك الأقل لحالة واجهة المستخدم.

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

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

ضَع في اعتبارك ConversationViewModel لتطبيق المحادثات وطريقة عرض حالة واجهة المستخدم على الشاشة والأحداث لتعديلها:

class ConversationViewModel(
    channelId: String,
    messagesRepository: MessagesRepository
) : ViewModel() {

    val messages = messagesRepository
        .getLatestMessages(channelId)
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )

    // Business logic
    fun sendMessage(message: Message) { /* ... */ }
}

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

في ما يلي مثال على ViewModel في عنصر قابل للإنشاء على مستوى الشاشة. في هذا المثال، يستهلك ConversationScreen() القابل للإنشاء حالة واجهة مستخدم الشاشة التي تم رفعها في ViewModel:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

الحفر العقاري

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

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

على الرغم من أنّ عرض الأحداث كمَعلمات lambda فردية قد يؤدي إلى زيادة الحمل على توقيع الدالة، فإنّه يزيد من ظهور مسؤوليات الدالة القابلة للإنشاء إلى أقصى حدّ. يمكنك إلقاء نظرة سريعة على ما تفعله هذه الميزة.

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

تنطبق أفضل الممارسات نفسها إذا كانت هذه الأحداث هي أحداث تنقّل، ويمكنك معرفة المزيد من المعلومات عن ذلك في مستندات التنقّل.

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

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

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

متابعة لمثال تطبيق محادثة، يعرض التطبيق اقتراحات المستخدم في محادثة جماعية عندما يكتب المستخدم @ وتلميحًا. تأتي هذه الاقتراحات من طبقة البيانات، ويعتبر منطق حساب قائمة اقتراحات المستخدم منطق العمل. تبدو الميزة كما يلي:

ميزة تعرض اقتراحات المستخدم في محادثة جماعية عندما يكتب المستخدم &quot;@&quot; وتلميحًا
الشكل 7. ميزة تعرض اقتراحات المستخدم في محادثة جماعية عندما يكتب المستخدم @ وتلميحًا

ستظهر الميزة التي توفّر هذه الميزة في ViewModel على النحو التالي:

class ConversationViewModel(/*...*/) : ViewModel() {

    // Hoisted state
    var inputMessage by mutableStateOf("")
        private set

    val suggestions: StateFlow<List<Suggestion>> =
        snapshotFlow { inputMessage }
            .filter { hasSocialHandleHint(it) }
            .mapLatest { getHandle(it) }
            .mapLatest { repository.getSuggestions(it) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = emptyList()
            )

    fun updateInput(newInput: String) {
        inputMessage = newInput
    }
}

inputMessage هو متغيّر يحفظ حالة TextField. في كل مرة يكتب فيها المستخدم إدخالاً جديدًا، يطلب التطبيق منطق النشاط التجاري لإنتاج suggestions.

suggestions هي حالة لواجهة مستخدم الشاشة ويتم استخدامها من Compose UI من خلال جمعها من StateFlow.

تنبيه

بالنسبة إلى بعض حالات عنصر Compose UI، قد يتطلب النقل إلى ViewModel اعتبارات خاصة. على سبيل المثال، يعرض بعض حاملي الحالة لعناصر Compose UI طرقًا لتعديل الحالة. قد يكون بعضها يعلق الدوال التي تؤدي إلى تشغيل الرسوم المتحركة. ويمكن أن تؤدي دوال التعليق هذه إلى حدوث استثناءات إذا استدعيتها من CoroutineScope غير مخصص للمقطوعة الموسيقية.

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

ومع ذلك، يؤدي استدعاء طريقة close() في DrawerState باستخدام viewModelScope من واجهة مستخدم Compose إلى استثناء وقت التشغيل من النوع IllegalStateException مع عرض رسالة المكتوب فيها "a MonotonicFrameClock" غير متاح في CoroutineContext”.

لحلّ هذه المشكلة، يمكنك استخدام السمة CoroutineScope المحدّدة للمقطوعة الموسيقية. توفِّر السمة MonotonicFrameClock في CoroutineContext، وهي عنصر ضروري لكي تعمل دوال التعليق.

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

class ConversationViewModel(/*...*/) : ViewModel() {

    val drawerState = DrawerState(initialValue = DrawerValue.Closed)

    private val _drawerContent = MutableStateFlow(DrawerContent.Empty)
    val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow()

    fun closeDrawer(uiScope: CoroutineScope) {
        viewModelScope.launch {
            withContext(uiScope.coroutineContext) { // Use instead of the default context
                drawerState.close()
            }
            // Fetch drawer content and update state
            _drawerContent.update { content }
        }
    }
}

// in Compose
@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {
    val scope = rememberCoroutineScope()

    ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) })
}

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

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

العيّنات

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

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