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

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

بهترین تمرین

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

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

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

انواع حالت UI و منطق UI

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

وضعیت رابط کاربری

حالت رابط کاربری خصوصیتی است که UI را توصیف می کند. دو نوع حالت رابط کاربری وجود دارد:

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

منطق

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

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

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

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

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

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

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

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

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

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

بالا بردن در مواد ترکیبی

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

مثال زیر یک برنامه چت است که دو عملکرد را اجرا می کند:

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

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

درخت قابل ترکیب چت
شکل 2. درخت قابل ترکیب چت

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

بالا بردن حالت LazyColumn از LazyColumn به ConversationScreen
شکل 3. بالا بردن حالت LazyColumn از LazyColumn به ConversationScreen

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

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

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

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

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

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

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

این کلاس های ساده در Composition ایجاد شده و به خاطر سپرده می شوند. از آنجا که آنها از چرخه حیات composable پیروی می کنند، می توانند انواع ارائه شده توسط کتابخانه Compose مانند rememberNavController() یا rememberLazyListState() را بگیرند.

نمونه ای از این کلاس دارنده حالت ساده LazyListState است که در Compose برای کنترل پیچیدگی UI 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 نیاز به یک دارنده دولت را افزایش می دهد . مسئولیت‌ها می‌توانند در منطق UI یا فقط در مقدار وضعیتی که باید پیگیری شود.

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

منطق کسب و کار

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

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

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

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

وقتی حالت UI را در ViewModel بالا می برید، آن را به خارج از Composition منتقل می کنید.

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

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

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

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

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

وضعیت عنصر UI

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

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

قابلیتی که وقتی کاربر «@» و یک اشاره را تایپ می‌کند، پیشنهادات کاربر را در یک چت گروهی نمایش می‌دهد
شکل 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 حالت رابط کاربری صفحه است و با جمع‌آوری از StateFlow از Compose UI مصرف می‌شود.

هشدار

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

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

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

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

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

نمونه ها

Codelabs

ویدیوها

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}