Bir Compose uygulamasında UI durumu, UI mantığı veya işletme mantığı tarafından gerekli olup olmamasına bağlı olarak yerleştirilir. Bu belgede, bu iki ana senaryo açıklanmaktadır.
En iyi uygulama
Kullanıcı arayüzü durumunu, okuma ve yazma işlemini yapan tüm composable'lar arasındaki en düşük ortak ataya yükseltmeniz gerekir. Durumu, tüketildiği yere en yakın tutmalısınız. Durum sahibinden, tüketicilere değişmez durum ve durumu değiştirmek için etkinlikler sunun.
En yakın ortak üst öğe, Composition dışında da olabilir. Örneğin, iş mantığı söz konusu olduğunda ViewModel içinde durum yükseltilirken.
Bu sayfada, bu en iyi uygulama ayrıntılı olarak açıklanmakta ve dikkat edilmesi gereken bir uyarıdan bahsedilmektedir.
Kullanıcı arayüzü durumu ve kullanıcı arayüzü mantığı türleri
Aşağıda, bu dokümanda kullanılan kullanıcı arayüzü durumu ve mantık türlerinin tanımları verilmiştir.
Kullanıcı arayüzü durumu
UI state (Kullanıcı arayüzü durumu), kullanıcı arayüzünü açıklayan özelliktir. İki tür kullanıcı arayüzü durumu vardır:
- Ekran kullanıcı arayüzü durumu, ekranda göstermeniz gereken şeydir. Örneğin, bir
NewsUiStatesınıfı, kullanıcı arayüzünü oluşturmak için gereken haber makalelerini ve diğer bilgileri içerebilir. Bu durum genellikle uygulama verilerini içerdiğinden hiyerarşinin diğer katmanlarıyla bağlantılıdır. - Kullanıcı arayüzü öğesi durumu, kullanıcı arayüzü öğelerine özgü olup nasıl oluşturulduklarını etkileyen özellikleri ifade eder. Bir kullanıcı arayüzü öğesi gösterilebilir veya gizlenebilir ve belirli bir yazı tipi, yazı tipi boyutu ya da yazı tipi rengine sahip olabilir. Jetpack Compose'da durum, composable'ın dışındadır ve hatta composable'ın hemen yakınından composable işlevini çağıran veya bir durum bilgisi depolayıcıya taşıyabilirsiniz. Buna örnek olarak,
Scaffoldcomposable'ı içinScaffoldStateverilebilir.
Mantık
Bir uygulamadaki mantık, iş mantığı veya kullanıcı arayüzü mantığı olabilir:
- İş mantığı, uygulama verileriyle ilgili ürün şartlarının uygulanmasıdır. Örneğin, kullanıcı düğmeye dokunduğunda bir haber okuyucu uygulamasında makaleye yer işareti ekleme. Bir yer işaretini dosyaya veya veritabanına kaydetme mantığı genellikle alan veya veri katmanlarına yerleştirilir. Durum bilgisi depolayıcı genellikle bu mantığı, sundukları yöntemleri çağırarak bu katmanlara devreder.
- Kullanıcı arayüzü mantığı, kullanıcı arayüzü durumunun ekranda nasıl görüntüleneceğiyle ilgilidir. Örneğin, kullanıcı bir kategori seçtiğinde doğru arama çubuğu ipucunu alma, listede belirli bir öğeye kaydırma veya kullanıcı bir düğmeyi tıkladığında belirli bir ekrana gitme mantığı.
Kullanıcı arayüzü mantığı
Kullanıcı arayüzü mantığının durumu okuması veya yazması gerektiğinde, durumu yaşam döngüsünü takip ederek kullanıcı arayüzüyle sınırlamanız gerekir. Bunu yapmak için durumu composable işlevde doğru düzeyde yükseltmeniz gerekir. Alternatif olarak, bunu kullanıcı arayüzü yaşam döngüsüyle de kapsamı belirlenmiş bir düz durum bilgisi depolayıcı sınıfında yapabilirsiniz.
Aşağıda, her iki çözümün açıklaması ve hangisinin ne zaman kullanılacağı ile ilgili bilgiler yer almaktadır.
Durum sahibi olarak composable'lar
Durum ve mantık basitse kullanıcı arayüzü mantığını ve kullanıcı arayüzü öğesi durumunu composable'larda tutmak iyi bir yaklaşımdır. Durumunuzu gerektiğinde composable bir işlevin içinde bırakabilir veya üst kapsamda kullanabilirsiniz.
Durum yükseltme gerekmez
Durumun yükseltilmesi her zaman gerekli değildir. Başka bir composable'ın kontrol etmesi gerekmediğinde durum, composable içinde dahili olarak tutulabilir. Bu snippet'te, dokunulduğunda genişleyen ve daralan bir composable var:
@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 değişkeni, bu kullanıcı arayüzü öğesinin dahili durumudur. Yalnızca bu composable'da okunur ve değiştirilir. Üzerine uygulanan mantık çok basittir.
Bu durumda durumu yükseltmek pek fayda sağlamayacağından durumu dahili olarak bırakabilirsiniz. Bu şekilde, bu composable, genişletilmiş durumun sahibi ve tek doğru kaynağı olur.
Composable'larda yükseltme
Kullanıcı arayüzü öğesi durumunuzu diğer composable'larla paylaşmanız ve kullanıcı arayüzü mantığını farklı yerlerde uygulamanız gerekiyorsa durumu kullanıcı arayüzü hiyerarşisinde daha yukarı taşıyabilirsiniz. Bu sayede composable işlevleriniz daha fazla yeniden kullanılabilir ve test edilmesi daha kolay hale gelir.
Aşağıdaki örnek, iki işlevi uygulayan bir sohbet uygulamasıdır:
JumpToBottomdüğmesi, mesaj listesini en alta kaydırır. Düğme, liste durumunda kullanıcı arayüzü mantığını yürütür.- Kullanıcı yeni mesajlar gönderdikten sonra
MessagesListlistesi en alta kaydırılıyor. UserInput, liste durumunda kullanıcı arayüzü mantığını yürütür.
JumpToBottom düğmesi olan sohbet uygulaması ve yeni mesajlarda en alta kaydırmaBirleştirilebilir hiyerarşi şöyledir:
LazyColumn durumu, uygulamanın kullanıcı arayüzü mantığını gerçekleştirebilmesi ve durumu, gerekli olan tüm composable'lardan okuyabilmesi için görüşme ekranına taşınır:
LazyColumn durumunu LazyColumn konumundan ConversationScreen konumuna taşımaSon olarak, composable işlevler şunlardır:
LazyListState öğesinin ConversationScreenKod aşağıdaki gibidir:
@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, uygulanması gereken kullanıcı arayüzü mantığı için gerektiği kadar yukarı taşınır. Composable işlevde başlatıldığından, yaşam döngüsünü takip ederek Composition'da depolanır.
lazyListState değerinin, varsayılan değeri rememberLazyListState() olan MessagesList yönteminde tanımlandığını unutmayın. Bu, Compose'da yaygın bir kalıptır.
Bu sayede composable işlevler daha fazla yeniden kullanılabilir ve esnek hale gelir. Daha sonra, durumu kontrol etmesi gerekmeyen uygulamanın farklı bölümlerinde composable'ı kullanabilirsiniz. Bu durum genellikle bir composable'ı test ederken veya önizlerken geçerlidir. LazyColumn, durumunu tam olarak bu şekilde tanımlar.
LazyListState için en yakın ortak üst öğe ConversationScreenDurum sahibi olarak düz durum bilgisi depolayıcı sınıfı
Bir composable, bir kullanıcı arayüzü öğesinin bir veya daha fazla durum alanını içeren karmaşık kullanıcı arayüzü mantığı içerdiğinde bu sorumluluğu, düz bir durum bilgisi depolayıcı sınıfı gibi durum bilgisi depolayıcılara devretmelidir. Bu, composable'ın mantığını izole bir şekilde daha test edilebilir hale getirir ve karmaşıklığını azaltır. Bu yaklaşım, ilgi alanlarını ayırma ilkesini destekler: Birleştirilebilir öğe, kullanıcı arayüzü öğelerini yayınlamaktan sorumludur ve durum tutucu, kullanıcı arayüzü mantığını ve kullanıcı arayüzü öğesi durumunu içerir.
Düz durum bilgisi depolayıcı sınıflar, composable işlevinizin arayanlarına kolaylık sağlayan işlevler sunar. Böylece, bu mantığı kendileri yazmak zorunda kalmazlar.
Bu düz sınıflar, birleştirmede oluşturulur ve hatırlanır. Composable'ın yaşam döngüsünü takip ettikleri için Compose kitaplığı tarafından sağlanan rememberNavController() veya rememberLazyListState() gibi türleri alabilirler.
Buna örnek olarak, LazyListState düz durum tutucu sınıfı verilebilir. Bu sınıf, LazyColumn veya LazyRow kullanıcı arayüzünün karmaşıklığını kontrol etmek için Compose'da uygulanır.
// 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, bu kullanıcı arayüzü öğesi için LazyColumn depolayan scrollPosition durumunu kapsar. Ayrıca, örneğin belirli bir öğeye kaydırarak kaydırma konumunu değiştirmek için yöntemler sunar.
Gördüğünüz gibi, bir composable'ın sorumluluklarını artırmak durum bilgisi depolayıcı ihtiyacını artırır. Sorumluluklar kullanıcı arayüzü mantığında veya yalnızca takip edilecek durum miktarıyla ilgili olabilir.
Bir diğer yaygın kalıp ise uygulamadaki kök composable işlevlerin karmaşıklığını yönetmek için düz bir durum tutucu sınıf kullanmaktır. Bu tür bir sınıfı, gezinme durumu ve ekran boyutlandırma gibi uygulama düzeyindeki durumu kapsüllemek için kullanabilirsiniz. Bununla ilgili eksiksiz bir açıklamayı Kullanıcı arayüzü mantığı ve durum bilgisi depolayıcısı sayfasında bulabilirsiniz.
İş mantığı
Composables ve düz durum bilgisi depolayıcı sınıflar kullanıcı arayüzü mantığından ve kullanıcı arayüzü öğesi durumundan sorumluyken ekran düzeyinde durum bilgisi depolayıcı aşağıdaki görevlerden sorumludur:
- Genellikle hiyerarşinin diğer katmanlarında (ör. işletme ve veri katmanları) yer alan uygulamanın iş mantığına erişim sağlama.
- Uygulama verilerini belirli bir ekranda sunulacak şekilde hazırlama (bu ekran, ekran kullanıcı arayüzü durumu olur).
Durum sahibi olarak ViewModel'ler
Android geliştirmede AAC ViewModel'lerin avantajları, bu ViewModel'leri iş mantığına erişim sağlamak ve uygulama verilerini ekranda sunulmaya hazırlamak için uygun hale getirir.
Kullanıcı arayüzü durumunu ViewModel içinde barındırdığınızda, durumu Composition'ın dışına taşırsınız.
ViewModel'ya yükseltilen durum, Composition dışında depolanır.ViewModel'ler, Composition'ın bir parçası olarak depolanmaz. Bunlar çerçeve tarafından sağlanır ve bir ViewModelStoreOwner ile sınırlıdır. Bu kapsam, bir etkinlik, parça, gezinme grafiği veya gezinme grafiğinin hedefi olabilir. ViewModel kapsamları hakkında daha fazla bilgi için dokümanları inceleyebilirsiniz.
Bu durumda, ViewModel, doğruluk kaynağı ve kullanıcı arayüzü durumu için en düşük ortak üst öğedir.
Ekran kullanıcı arayüzü durumu
Yukarıdaki tanımlara göre, ekran kullanıcı arayüzü durumu işletme kuralları uygulanarak oluşturulur. Ekran düzeyindeki durum bilgisi depolayıcı bundan sorumlu olduğundan ekran kullanıcı arayüzü durumu genellikle ekran düzeyindeki durum bilgisi depolayıcıda (bu örnekte ViewModel) yükseltilir.
Bir sohbet uygulamasının ConversationViewModel ve ekran kullanıcı arayüzü durumunu ve bunu değiştirmek için etkinlikleri nasıl ortaya çıkardığını düşünün:
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 içinde yükseltilen ekran kullanıcı arayüzü durumunu kullanır. İş mantığına erişim sağlamak için ekran düzeyindeki composable'larınıza ViewModel örneğini yerleştirmeniz gerekir.
Aşağıda, ekran düzeyinde bir composable'da kullanılan ViewModel örneği verilmiştir.
Burada, ConversationScreen() composable'ı, ViewModel içinde yükseltilen ekran kullanıcı arayüzü durumunu kullanır:
@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) /* ... */ }
Mülk sondajı
"Mülk detayı", verilerin okunacakları konuma ulaşmak için iç içe yerleştirilmiş birkaç alt bileşenden geçirilmesini ifade eder.
Compose'da özellik ayrıntılandırmanın görünebileceği tipik bir örnek, ekran düzeyindeki durum bilgisi depolayıcıyı üst düzeyde yerleştirdiğiniz ve durumu ile etkinlikleri alt composable'lara aktardığınız zamandır. Bu durum, ayrıca birleştirilebilir işlev imzalarının aşırı yüklenmesine neden olabilir.
Etkinlikleri ayrı lambda parametreleri olarak kullanmak işlev imzasını aşırı yükleyebilse de composable işlevinin sorumluluklarının görünürlüğünü en üst düzeye çıkarır. Ne yaptığını bir bakışta görebilirsiniz.
Durumu ve etkinlikleri tek bir yerde kapsüllemek için sarmalayıcı sınıflar oluşturmak yerine özellik detayı tercih edilir. Çünkü bu, composable sorumluluklarının görünürlüğünü azaltır. Sarmalayıcı sınıflarınız olmadığında, composable'lara yalnızca ihtiyaç duydukları parametreleri iletme olasılığınız da artar. Bu, en iyi uygulamalardan biridir.
Bu etkinlikler gezinme etkinlikleriyse aynı en iyi uygulama geçerlidir. Bu konuda daha fazla bilgiyi gezinme belgelerinde bulabilirsiniz.
Bir performans sorunu tespit ettiyseniz durumun okunmasını ertelemeyi de seçebilirsiniz. Daha fazla bilgi edinmek için performans belgelerine göz atabilirsiniz.
Kullanıcı arayüzü öğesi durumu
Okunması veya yazılması gereken bir iş mantığı varsa kullanıcı arayüzü öğesi durumunu ekran düzeyindeki durum bilgisi depolayıcıya yükseltebilirsiniz.
Sohbet uygulaması örneğine devam edersek uygulama, kullanıcı @ yazıp bir ipucu girdiğinde grup sohbetinde kullanıcı önerileri gösterir. Bu öneriler, veri katmanından gelir ve kullanıcı önerileri listesini hesaplama mantığı, iş mantığı olarak kabul edilir. Bu özellik şu şekilde görünür:
@ yazıp ipucu girdiğinde grup sohbetinde kullanıcı önerilerini gösteren özellikBu özelliği uygulayan ViewModel aşağıdaki gibi görünür:
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 durumunu depolayan bir değişkendir. Kullanıcı her yeni giriş yaptığında uygulama, suggestions oluşturmak için iş mantığını çağırır.
suggestions, ekran kullanıcı arayüzü durumudur ve StateFlow'den toplanarak Compose kullanıcı arayüzünde kullanılır.
Caveat
Bazı Compose kullanıcı arayüzü öğesi durumları için ViewModel'ya yükseltme işlemi özel dikkat gerektirebilir. Örneğin, Compose kullanıcı arayüzü öğelerinin bazı durum sahipleri, durumu değiştirmek için yöntemler sunar. Bunlardan bazıları, animasyonları tetikleyen askıya alma işlevleri olabilir. Bu askıya alma işlevleri, bunları Composition kapsamına alınmamış bir CoroutineScope içinden çağırırsanız istisna oluşturabilir.
Uygulama çekmecesinin içeriğinin dinamik olduğunu ve kapatıldıktan sonra veri katmanından getirilip yenilenmesi gerektiğini varsayalım. Çekmece durumunu ViewModel konumuna yükseltmeniz gerekir. Böylece, bu öğede hem kullanıcı arayüzünü hem de işletme mantığını durum sahibinden çağırabilirsiniz.
Ancak Compose kullanıcı arayüzündeki viewModelScope kullanılarak DrawerState'nin close() yöntemi çağrıldığında, "MonotonicFrameClock bu CoroutineContext” içinde kullanılamaz" mesajıyla IllegalStateException türünde bir çalışma zamanı istisnası oluşur.
Bu sorunu düzeltmek için Kompozisyon kapsamlı bir CoroutineScope kullanın. Askıya alma işlevlerinin çalışması için gerekli olan CoroutineContext içinde MonotonicFrameClock sağlar.
Bu kilitlenmeyi düzeltmek için ViewModel içindeki eş yordamın CoroutineContext değerini, Composition kapsamına alınmış bir değerle değiştirin. Şu şekilde görünebilir:
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) }) }
Daha fazla bilgi
State ve Jetpack Compose hakkında daha fazla bilgi edinmek için aşağıdaki ek kaynaklara göz atın.
Örnekler
Codelab uygulamaları
Videolar
Sizin için önerilenler
- Not: JavaScript kapalıyken bağlantı metni gösterilir.
- Compose'da kullanıcı arayüzü durumunu kaydetme
- Listeler ve ızgaralar
- Compose kullanıcı arayüzünüzü tasarlama