در یک برنامه 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 را در وضعیت لیست انجام می دهد.
سلسله مراتب قابل ترکیب به شرح زیر است:
حالت LazyColumn
روی صفحه مکالمه قرار میگیرد تا برنامه بتواند منطق UI را انجام دهد و وضعیت را از همه اجزای سازنده که به آن نیاز دارند بخواند:
بنابراین در نهایت ترکیب پذیرها عبارتند از:
کد به شرح زیر است:
@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
دقیقاً وضعیت خود را اینگونه تعریف می کند.
کلاس دارنده ایالت ساده به عنوان مالک ایالت
هنگامی که یک 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 ذخیره نمی شوند. آنها توسط چارچوب ارائه شدهاند و به 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 را به نگهدارنده حالت سطح صفحه ببرید.
در ادامه مثال یک برنامه چت، برنامه زمانی که کاربر @
و یک اشاره را تایپ می کند، پیشنهادات کاربر را در یک چت گروهی نمایش می دهد. این پیشنهادها از لایه داده می آیند و منطق محاسبه لیست پیشنهادات کاربر، منطق تجاری در نظر گرفته می شود. ویژگی به شکل زیر است:
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
ویدیوها
{% کلمه به کلمه %}برای شما توصیه می شود
- توجه: وقتی جاوا اسکریپت خاموش است، متن پیوند نمایش داده می شود
- حالت رابط کاربری را در Compose ذخیره کنید
- فهرست ها و شبکه ها
- معماری UI Compose شما