محل بالا بردن حالت

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

بهترین شیوه

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

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

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

انواع حالت‌های رابط کاربری و منطق رابط کاربری

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

حالت رابط کاربری

وضعیت رابط کاربری (UI state ) ویژگی‌ای است که رابط کاربری (UI) را توصیف می‌کند. دو نوع وضعیت رابط کاربری وجود دارد:

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

منطق

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

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

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

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

در زیر توضیحی از هر دو راه حل و توضیح اینکه چه زمانی از کدام استفاده کنید، آورده شده است.

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

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

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

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

بالا بردن درون کامپوننت‌ها

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

مثال زیر یک برنامه چت است که دو بخش از قابلیت‌ها را پیاده‌سازی می‌کند:

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

سلسله مراتب ترکیب به شرح زیر است:

درخت ترکیبی چت
شکل ۲. درخت ترکیب‌پذیر چت

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

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

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

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

توجه داشته باشید که lazyListState در متد MessagesList با مقدار پیش‌فرض rememberLazyListState() تعریف شده است. این یک الگوی رایج در Compose است. این امر باعث می‌شود که composableها قابلیت استفاده مجدد و انعطاف‌پذیری بیشتری داشته باشند. سپس می‌توانید از composable در قسمت‌های مختلف برنامه که ممکن است نیازی به کنترل وضعیت نداشته باشند، استفاده کنید. این معمولاً هنگام آزمایش یا پیش‌نمایش یک composable اتفاق می‌افتد. LazyColumn دقیقاً به همین شکل وضعیت خود را تعریف می‌کند.

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

دارنده ایالت ساده به عنوان مالک ایالتی

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

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

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

همانطور که می‌بینید، افزایش مسئولیت‌های یک composable نیاز به یک نگهدارنده‌ی وضعیت (state holder) را افزایش می‌دهد . این مسئولیت‌ها می‌توانند در منطق رابط کاربری یا فقط در مقدار وضعیتی باشند که باید پیگیری شود.

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

منطق کسب و کار

اگر کلاس‌های composables و plain state holders مسئول منطق رابط کاربری و وضعیت عنصر رابط کاربری باشند، یک screen level state holder مسئول وظایف زیر است:

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

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

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

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

وضعیت (state) که به ViewModel منتقل می‌شود، خارج از Composition ذخیره می‌شود.
شکل ۶. وضعیت (state) منتقل شده به ViewModel در خارج از Composition ذخیره می‌شود.

ViewModelها به عنوان بخشی از Composition ذخیره نمی‌شوند. آن‌ها توسط فریم‌ورک ارائه می‌شوند و به ViewModelStoreOwner که می‌تواند یک Activity، Fragment، گراف ناوبری یا مقصد یک گراف ناوبری باشد، محدود می‌شوند. برای اطلاعات بیشتر در مورد محدوده‌های ViewModel می‌توانید مستندات را بررسی کنید.

سپس، ViewModel منبع حقیقت و پایین‌ترین جد مشترک برای حالت UI است.

وضعیت رابط کاربری صفحه نمایش

طبق تعاریف بالا، وضعیت رابط کاربری صفحه نمایش با اعمال قوانین تجاری تولید می‌شود. با توجه به اینکه نگهدارنده وضعیت سطح صفحه نمایش مسئول آن است، این بدان معناست که وضعیت رابط کاربری صفحه نمایش معمولاً در نگهدارنده وضعیت سطح صفحه نمایش، که در این مورد یک 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) { /* ... */ }
}

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

در ادامه مثالی از یک 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های فرزند منتقل می‌کنید. این ممکن است علاوه بر این، باعث ایجاد سربار زیادی از امضاهای توابع composable شود.

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

حفاری ویژگی‌ها (Property drilling) نسبت به ایجاد کلاس‌های پوششی (wrapper classes) برای کپسوله‌سازی حالت و رویدادها در یک مکان ارجحیت دارد، زیرا این کار باعث کاهش قابلیت مشاهده مسئولیت‌های composable می‌شود. با نداشتن کلاس‌های پوششی، احتمال بیشتری وجود دارد که 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 UI مصرف می‌شود.

هشدار

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

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

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

برای رفع این مشکل، از یک CoroutineScope که به Composition محدود شده است استفاده کنید. این تابع یک MonotonicFrameClock در CoroutineContext فراهم می‌کند که برای عملکرد توابع suspend ضروری است.

برای رفع این مشکل، CoroutineContext مربوط به coroutine در ViewModel را به مقداری تغییر دهید که به Composition محدود شود. این می‌تواند چیزی شبیه به این باشد:

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، به منابع اضافی زیر مراجعه کنید.

نمونه‌ها

کدلبز

ویدیوها

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}