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

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

روال مطلوب

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

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

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

انواع حالت واسط کاربر و منطق واسط کاربر

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

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

حالت واسط کاربر ویژگی‌ای است که واسط کاربر را توصیف می‌کند. دو نوع وضعیت میانای کاربر وجود دارد:

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

منطق

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

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

منطق واسط کاربر

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

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

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

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

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

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

@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 وضعیت داخلی این عنصر میانای کاربر است. فقط در این composable خوانده و اصلاح می‌شود و منطق اعمال‌شده بر آن بسیار ساده است. بنابراین، بالا بردن وضعیت در این مورد مزیت چندانی نخواهد داشت، بنابراین می‌توانید آن را داخلی بگذارید. با انجام این کار، این composable مالک و منبع واحد حقیقت حالت گسترده می‌شود.

بالا بردن در ترکیب‌پذیرها

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

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

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

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

درخت قابل‌ترکیب گپ
شکل ۲. درخت ترکیب‌پذیر گپ

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

بالا بردن وضعیت LazyColumn از LazyColumn به ConversationScreen
شکل ۳.‏ Hoisting LazyColumn state from the LazyColumn to the ConversationScreen

So finally the composables are:

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

پایین‌ترین جد مشترک برای LazyListState،‏ ConversationScreen است
شکل ۵. پایین‌ترین جد مشترک برای LazyListState ConversationScreen است

کلاس نگه‌دارنده حالت ساده به‌عنوان مالک حالت

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

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

این کلاس‌های ساده در Composition ایجاد و به‌خاطر سپرده می‌شوند. چون آن‌ها چرخه حیات ترکیب‌پذیر را دنبال می‌کنند، می‌توانند انواع ارائه‌شده توسط کتابخانه 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 را برای این عنصر میانای کاربر ذخیره می‌کند، کپسوله‌سازی می‌کند. همچنین روش‌هایی را برای تغییر موقعیت پیمایش، مثلاً با پیمایش به یک مورد خاص، ارائه می‌دهد.

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

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

منطق کسب‌وکار

اگر کلاس‌های نگه‌دارنده حالت ساده و قابل‌ترکیب مسئول منطق میانای کاربر و حالت عنصر میانای کاربر باشند، نگه‌دارنده حالت سطح صفحه مسئول وظایف زیر است:

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

‫ViewModel به‌عنوان مالک وضعیت

مزایای AAC ViewModels در توسعه Android آن‌ها را برای ارائه دسترسی به منطق کسب‌وکار و آماده‌سازی داده‌های برنامه برای ارائه روی صفحه مناسب می‌کند.

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

وضعیت بالابرده‌شده به ViewModel خارج از «ترکیب‌بندی» ذخیره می‌شود.
شکل ۶. حالت بالابرده‌شده به ViewModel خارج از «ترکیب‌بندی» ذخیره می‌شود.

‫ViewModelها به‌عنوان بخشی از Composition ذخیره نمی‌شوند. این‌ها توسط چارچوب ارائه می‌شوند و در محدوده ViewModelStoreOwner قرار دارند که می‌تواند یک Activity،‏ Fragment، نمودار پیمایش، یا مقصد یک نمودار پیمایش باشد. برای کسب اطلاعات بیشتر درباره گستره‌های 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) { /* ... */ }
}

‫Composables از حالت میانای کاربر صفحه که در ViewModel بالا برده شده است استفاده می‌کند. باید نمونه ViewModel را در ترکیب‌شدنی‌های سطح صفحه‌نمایش تزریق کنید تا دسترسی به منطق کسب‌وکار را فراهم کنید.

در زیر نمونه‌ای از ViewModel استفاده‌شده در یک ترکیب‌پذیر سطح صفحه ارائه شده است. در اینجا، composable 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 ظاهر شود، زمانی است که نگه‌دارنده حالت سطح صفحه را در سطح بالا تزریق می‌کنید و حالت و رویدادها را به سازه‌های فرزند منتقل می‌کنید. این کار ممکن است علاوه‌براین باعث ایجاد اضافه‌بار در امضاهای تابع ترکیب‌پذیر شود.

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

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

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

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

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

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

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

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

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 ارتقا دهید تا بتوانید هم منطق کسب‌وکار و هم واسط کاربر را در این عنصر از مالک وضعیت فراخوانی کنید.

بااین‌حال، فراخوانی روش close() مربوط به DrawerState بااستفاده از viewModelScope از Compose UI باعث ایجاد استثنای زمان اجرا از نوع IllegalStateException با پیامی می‌شود که می‌گوید «یک MonotonicFrameClock در این CoroutineContext” دردسترس نیست.

برای رفع این مشکل، از CoroutineScope با دامنه Composition استفاده کنید. این مؤلفه یک 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

ویدیوها