Bir Compose uygulamasında kullanıcı arayüzü durumunu yükseltme, kullanıcı arayüzü mantığı veya işletme mantığının bunu gerektirip gerektirmediğine bağlıdır. Bu belgede, bu iki ana senaryo açıklanmaktadır.
En iyi uygulama
Kullanıcı arayüzü durumunu, okuma ve yazma işlemleri yapan tüm composable'lar arasındaki en düşük ortak ataya yükseltmeniz gerekir. Durumu, kullanıldığı yere en yakın tutmanız gerekir. Durum sahibinden, tüketicilere değişmez durum ve durumu değiştirmek için etkinlikler sunun.
En yakın ortak üst öğe, Kompozisyon'un dışında da olabilir. Örneğin, iş mantığı söz konusu olduğundan ViewModel içinde durum yükseltilirken.
Bu sayfada, bu en iyi uygulama ayrıntılı olarak açıklanmakta ve dikkat edilmesi gereken bir nokta belirtilmektedir.
Kullanıcı arayüzü durumu ve kullanıcı arayüzü mantığı türleri
Aşağıda, bu belgede 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ü öğelerinin oluşturulma şeklini etkileyen, öğelere özgü ö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 çağıran composable işlevine veya bir durum bilgisi depolayıcıya taşıyabilirsiniz. Bunun bir örneği,
Scaffoldcomposable'ı içinScaffoldStateolabilir.
Mantık
Bir uygulamadaki mantık, iş mantığı veya kullanıcı arayüzü mantığı olabilir:
- İş mantığı, uygulama verileriyle ilgili ürün koşullarının uygulanmasıdır. Örneğin, kullanıcı düğmeye dokunduğunda bir haber okuyucu uygulamasında makaleye yer işareti ekleme. Bir dosyaya veya veritabanına yer işareti 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ü izleyerek 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ğıyla ilgili bilgiler yer almaktadır.
Durum sahibi olarak composable'lar
Durum ve mantık basitse composable'larda kullanıcı arayüzü mantığı ve kullanıcı arayüzü öğesi durumunun olması iyi bir yaklaşımdır. Durumunuzu gerektiğinde birleştirilebilir bir öğenin içinde bırakabilir veya yukarı taşıyabilirsiniz.
Durum yükseltme gerekmez
Durumu yükseltmek 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'lar içinde taşıma
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ır. UserInput, liste durumunda kullanıcı arayüzü mantığını yürütür.
JumpToBottom düğmesi olan bir 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 gerektiren 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ı çekilir. Composable işlevde başlatıldığından, yaşam döngüsünü takip ederek Composition'da depolanır.
lazyListState değerinin, MessagesList yönteminde rememberLazyListState() varsayılan değeriyle 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 görülür. 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, LazyColumn veya LazyRow kullanıcı arayüzü karmaşıklığını kontrol etmek için Compose'da uygulanan LazyListState düz durum bilgisi depolayıcı sınıfı verilebilir.
// 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, kaydırma konumunu değiştirmek için yöntemler de sunar (ör. belirli bir öğeye kaydırma).
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. Bu konuyla ilgili eksiksiz bir açıklamayı UI logic and its state holder page (Kullanıcı arayüzü mantığı ve durum bilgisi depolayıcısı sayfası) bölümünde bulabilirsiniz.
İş mantığı
Kullanıcı arayüzü mantığı ve kullanıcı arayüzü öğesi durumundan composable'lar ve düz durum tutucu sınıflar sorumluysa ekran düzeyinde bir durum tutucu aşağıdaki görevlerden sorumludur:
- Genellikle işletme ve veri katmanları gibi hiyerarşinin diğer katmanlarına yerleştirilen 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 haline gelir.
Durum sahibi olarak ViewModel'ler
Android geliştirmede AAC ViewModels'in avantajları, bu bileşenleri 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 taşıdığınızda, durumu Composition'ın dışına taşımış olursunuz.
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 tutucu bundan sorumlu olduğundan, ekran kullanıcı arayüzü durumu genellikle ekran düzeyindeki durum tutucuda (bu durumda 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) { /* ... */ } }
Composable'lar, 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 okunacağı konuma ulaşmak için iç içe yerleştirilmiş birkaç alt bileşenden geçirilmesini ifade eder.
Mülk ayrıntılandırmanın Compose'da görünebileceği tipik bir örnek, ekran düzeyindeki durum tutucuyu ü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, birleştirilebilir işlevin 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 aktarımı 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 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 tutucuya 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ü öğe 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 taşıyarak hem kullanıcı arayüzünü hem de işletme mantığını durum sahibinden bu öğede ç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 bir 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. Şöyle 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