محل بالا بردن وضعیت

در برنامه «نوشتن»، اینکه وضعیت واسط کاربر را کجا بالا ببرید به این بستگی دارد که آیا منطق واسط کاربر یا منطق کسب‌وکار به آن نیاز دارد یا نه. این سند این دو سناریو اصلی را تشریح می‌کند.

روال مطلوب

باید وضعیت واسط کاربر را به پایین‌ترین جد مشترک بین همه عناصر ترکیبی که آن را می‌خوانند و می‌نویسند منتقل کنید. باید وضعیت را نزدیک‌ترین به جایی که مصرف می‌شود نگه دارید. از مالک وضعیت، وضعیت و رویدادهای تغییرناپذیر را دراختیار مصرف‌کنندگان قرار دهید تا وضعیت را تغییر دهند.

جد مشترک می‌تواند خارج از «ترکیب» نیز باشد. برای مثال، وقتی وضعیت در ViewModel بالابرده می‌شود چون منطق کسب‌وکار درگیر است.

این صفحه این روال مطلوب را به‌طور مفصل توضیح می‌دهد و هشداری را که باید به‌خاطر داشته باشید ارائه می‌دهد.

انواع وضعیت واسط کاربر و منطق واسط کاربر

در زیر، تعاریفی برای انواع وضعیت و منطق واسط کاربر که در سراسر این سند استفاده می‌شود ارائه شده است.

وضعیت میانای کاربر

وضعیت میانای کاربر مشخصه‌ای است که میانای کاربر را توصیف می‌کند. دو نوع وضعیت واسط کاربر وجود دارد:

  • وضعیت واسط کاربر صفحه چیزی است که باید در صفحه نمایش دهید. برای مثال، یک کلاس NewsUiState می‌تواند حاوی مقاله‌های خبری و اطلاعات دیگری باشد که برای پرداز کردن واسط کاربر لازم است. این وضعیت معمولاً با لایه‌های دیگر سلسله‌مراتب مرتبط است زیرا حاوی داده‌های برنامه است.
  • وضعیت عنصر واسط کاربر به ویژگی‌های ذاتی عناصر واسط کاربر اشاره دارد که بر نحوه پرداز آن‌ها تأثیر می‌گذارد. عنصر میانای کاربر ممکن است نشان داده شود یا پنهان شود و ممکن است قلم، اندازه قلم، یا رنگ قلم خاصی داشته باشد. در Jetpack Compose، وضعیت خارج از عنصر ترکیبی است و حتی می‌توانید آن را از مجاورت فوری عنصر ترکیبی به تابع عنصر ترکیبی فراخوان یا نگهدارنده وضعیت منتقل کنید. نمونه‌ای از این مورد ScaffoldState برای Scaffold قابل ترکیب است.

منطق

منطق در یک برنامه می‌تواند منطق کسب‌وکار یا منطق رابط کاربری باشد:

  • منطق کسب‌وکار اجرای الزامات محصول برای داده‌های برنامه است. برای مثال، نشان‌گذاری مقاله در برنامه خبرخوان وقتی کاربر روی دکمه ضربه می‌زند. این منطق برای ذخیره کردن نشانک در فایل یا پایگاه داده معمولاً در لایه‌های دامنه یا داده قرار می‌گیرد. نگه‌دارنده وضعیت معمولاً این منطق را با فراخوانی روش‌هایی که آشکار می‌کنند به آن لایه‌ها واگذار می‌کند.
  • منطق واسط کاربر مربوط به نحوه نمایش وضعیت واسط کاربر روی صفحه است. برای مثال، دریافت اشاره نوار جستجوی صحیح زمانی که کاربر دسته‌ای را انتخاب کرده است، پیمایش به مورد خاصی در فهرست، یا منطق پیمایش به صفحه‌ای خاص زمانی که کاربر روی دکمه‌ای کلیک می‌کند.

منطق میانای کاربر

وقتی منطق واسط کاربر نیاز به خواندن یا نوشتن وضعیت دارد، باید وضعیت را به واسط کاربر محدود کنید و از چرخه حیات آن پیروی کنید. برای دستیابی به این هدف، باید وضعیت را در سطح صحیح در یک تابع ترکیب‌پذیر بالا ببرید. یا می‌توانید این کار را در کلاس نگهدارنده وضعیت ساده انجام دهید که آن هم به چرخه حیات واسط کاربر محدود می‌شود.

در زیر، شرحی از هر دو راهکار و توضیحاتی درباره زمان استفاده از هرکدام ارائه شده است.

عناصر ترکیبی به‌عنوان مالک وضعیت

اگر وضعیت و منطق ساده باشند، داشتن منطق واسط کاربر و وضعیت عنصر واسط کاربر در عناصر ترکیب‌پذیر رویکرد خوبی است. می‌توانید وضعیت داخلی خود را در یک عنصر ترکیبی قرار دهید یا درصورت نیاز آن را بالا ببرید.

بالابردن حالت لازم نیست

بالا بردن وضعیت همیشه لازم نیست. وقتی هیچ عنصر ترکیبی دیگری نیاز به کنترل وضعیت ندارد، وضعیت می‌تواند در یک عنصر ترکیبی داخلی نگه داشته شود. در این گلچین، یک عنصر ترکیبی وجود دارد که با تک‌ضرب زدن ازهم باز می‌شود و جمع می‌شود:

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

    Text(
        text = AnnotatedString(message.content),
        modifier = Modifier.clickable {
            showDetails = !showDetails // Apply UI logic
        }
    )

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

متغیر showDetails وضعیت داخلی این عنصر میانای کاربر است. این متغیر فقط در این عنصر ترکیبی خوانده و اصلاح می‌شود و منطق اعمال‌شده روی آن بسیار ساده است. بنابراین، بالا بردن وضعیت در این مورد فایده چندانی ندارد، بنابراین می‌توانید آن را داخلی بگذارید. انجام این کار باعث می‌شود که این عنصر ترکیبی مالک و تنها منبع حقیقت وضعیت ازهم بازشده باشد.

بالا بردن در عناصر ترکیبی

اگر لازم است وضعیت عنصر میانای کاربر را با دیگر عناصر ترکیبی هم‌رسانی کنید و منطق میانای کاربر را در مکان‌های مختلف روی آن اعمال کنید، می‌توانید آن را در سلسله‌مراتب میانای کاربر به سطح بالاتری ارتقا دهید. این کار همچنین باعث می‌شود عناصر ترکیبی شما قابل استفاده مجدد و آزمایش آسان‌تر باشند.

مثال زیر برنامه گپی است که دو عملکرد را پیاده‌سازی می‌کند:

  • دکمه JumpToBottom فهرست پیام‌ها را به پایین پیمایش می‌کند. این دکمه منطق واسط کاربر را روی وضعیت فهرست اجرا می‌کند.
  • فهرست MessagesList پس‌از اینکه کاربر پیام‌های جدید ارسال می‌کند به پایین پیمایش می‌کند. ‫UserInput منطق واسط کاربر را در وضعیت فهرست اجرا می‌کند.
برنامه گپ با دکمه «رفتن به پایین» و پیمایش به پایین در پیام‌های جدید
شکل ۱. برنامه گپ با دکمه JumpToBottom و پیمایش به پایین در پیام‌های جدید

سلسله‌مراتب ترکیبی به‌صورت زیر است:

درخت Chat composable
شکل ۲. درخت عنصر ترکیبی گپ

وضعیت LazyColumn به صفحه مکالمه ارتقا می‌یابد تا برنامه بتواند منطق میانای کاربر را اجرا کند و وضعیت را از همه عناصر ترکیبی که به آن نیاز دارند بخواند:

انتقال وضعیت LazyColumn از LazyColumn به ConversationScreen
شکل ۳. درحال بالا بردن وضعیت LazyColumn از LazyColumn به ConversationScreen

بنابراین درنهایت، عناصر ترکیبی عبارت‌اند از:

درخت چت با LazyListState به ConversationScreen ارتقا یافت
شکل ۴. درخت عنصر ترکیبی 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() تعریف شده است. این الگویی رایج در «نوشتن» است. این کار باعث می‌شود عناصر ترکیبی انعطاف‌پذیرتر و قابل‌استفاده مجدد باشند. سپس می‌توانید از عنصر ترکیبی در بخش‌های مختلف برنامه که ممکن است نیازی به کنترل وضعیت نداشته باشند استفاده کنید. این معمولاً درحین آزمایش یا پیش‌نمایش یک عنصر ترکیبی اتفاق می‌افتد. دقیقاً به همین شکل LazyColumn وضعیت خود را تعریف می‌کند.

«صفحه مکالمه» پایین‌ترین جد مشترک برای LazyListState است
شکل ۵. جد مشترک 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» در توسعه Android باعث می‌شود این مدل‌ها برای فراهم کردن دسترسی به منطق کسب‌وکار و آماده کردن داده‌های برنامه برای ارائه در صفحه مناسب باشند.

وقتی وضعیت واسط کاربر را در ViewModel بالا می‌برید، آن را به خارج از «ترکیب» منتقل می‌کنید.

وضعیت بالابرده‌شده به ViewModel در خارج از «ترکیب» ذخیره می‌شود.
شکل ۶. وضعیت برافراشته‌شده به 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)
    /* ... */
}

حفاری ملک

«کاوش دارایی» به انتقال داده‌ها ازطریق چندین عنصر فرزند تودرتو به مکانی که در آن خوانده می‌شوند اشاره دارد.

نمونه‌ای معمول از جایی که حفاری دارایی می‌تواند در «نوشتن» ظاهر شود زمانی است که نگه‌دارنده وضعیت سطح صفحه را در سطح بالا تزریق می‌کنید و وضعیت و رویدادها را به عناصر ترکیبی فرزندان منتقل می‌کنید. این کار ممکن است علاوه‌براین باعث تولید بیش‌ازحد امضاهای تابع ترکیب‌شدنی شود.

اگرچه آشکار کردن رویدادها به‌عنوان پارامترهای لامبدای جداگانه می‌تواند امضای تابع را بیش‌ازحد بار کند، اما این کار نمایان بودن مسئولیت‌های تابع ترکیبی را به حداکثر می‌رساند. می‌توانید در یک نگاه ببینید چه کاری انجام می‌دهد.

حفاری دارایی بر ایجاد کلاس‌های بسته‌بندی برای کپسوله کردن وضعیت و رویدادها در یک مکان ترجیح داده می‌شود زیرا این کار نمایان بودن مسئولیت‌های ترکیب‌شدنی را کاهش می‌دهد. با نداشتن کلاس‌های بسته‌بندی، احتمال اینکه ترکیب‌پذیرها را فقط با پارامترهای موردنیازشان ارسال کنید بیشتر می‌شود که روشی مناسب است.

همین رویه مطلوب درصورتی‌که این رویدادها رویدادهای پیمایش باشند نیز اعمال می‌شود، می‌توانید در سندهای پیمایش درباره آن بیشتر بدانید.

اگر مشکل عملکردی را شناسایی کرده‌اید، می‌توانید خواندن وضعیت را نیز به‌تعویق بیندازید. برای کسب اطلاعات بیشتر، می‌توانید اسناد عملکرد را بررسی کنید.

وضعیت عنصر میانای کاربر

اگر منطق کسب‌وکاری وجود دارد که باید وضعیت عنصر میانای کاربر را بخواند یا بنویسد، می‌توانید وضعیت عنصر میانای کاربر را به نگهدارنده وضعیت سطح صفحه ارتقا دهید.

با ادامه مثال برنامه گپ، برنامه وقتی کاربر @ و یک اشاره را تایپ می‌کند، پیشنهادهای کاربر را در گپ گروهی نمایش می‌دهد. این پیشنهادها از لایه داده می‌آیند و منطق محاسبه فهرست پیشنهادهای کاربر منطق کسب‌وکار درنظر گرفته می‌شود. این ویژگی به این شکل است:

ویژگی‌ای که وقتی کاربر «@» و یک اشاره را تایپ می‌کند، پیشنهادهای کاربر را در گپ گروهی نمایش می‌دهد
شکل ۷. ویژگی‌ای که وقتی کاربر @ و یک اشاره
را تایپ می‌کند، پیشنهادهای کاربر را در گپ گروهی نمایش می‌دهد

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 وضعیت میانای کاربر صفحه است و با جمع‌آوری از StateFlow در «میانای کاربر Compose» مصرف می‌شود.

هشدار

برای برخی‌از وضعیت‌های عنصر «میانای کاربری Compose»، بالا بردن به ViewModel ممکن است نیاز به ملاحظات ویژه داشته باشد. برای مثال، برخی‌از نگهدارنده‌های حالت عناصر «میانای کاربر Compose» روش‌هایی را برای اصلاح حالت آشکار می‌کنند. برخی‌از آن‌ها ممکن است تابع‌های تعلیق باشند که پویانمایی‌ها را راه‌اندازی می‌کنند. اگر این توابع تعلیق را از CoroutineScope که در محدوده «ترکیب» نیست فراخوانی کنید، ممکن است استثناهایی ایجاد کنند.

فرض کنیم محتوای کشوی برنامه پویا است و باید پس‌از بسته شدن آن، محتوا را از لایه داده واکشی و بازآوری کنید. باید وضعیت کشو را به ViewModel بالا ببرید تا بتوانید هم واسط کاربر و هم منطق کسب‌وکار را در این عنصر از مالک وضعیت فراخوانی کنید.

بااین‌حال، فراخوانی روش DrawerState در close() بااستفاده از viewModelScope از Compose UI باعث استثنای زمان اجرا از نوع IllegalStateException با پیامی می‌شود که می‌گوید « 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) })
}

بیشتر بدانید

برای کسب اطلاعات بیشتر درباره حالت و Jetpack Compose، به منابع تکمیلی زیر مراجعه کنید.

نمونه‌ها

Codelabs

ویدیوها