Eyalet nereye kaldırılır?

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 NewsUiState sı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, Scaffold composable'ı için ScaffoldState verilebilir.

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:

  • JumpToBottom düğ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 MessagesList listesi en alta kaydırılıyor. UserInput, liste durumunda kullanıcı arayüzü mantığını yürütür.
JumpToBottom düğmesi olan ve yeni mesajlarda en alta kaydıran sohbet uygulaması
Şekil 1. JumpToBottom düğmesi olan sohbet uygulaması ve yeni mesajlarda en alta kaydırma

Birleştirilebilir hiyerarşi şöyledir:

Chat composable tree
Şekil 2. Chat composable tree

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'dan ConversationScreen'e yükseltme
Şekil 3. LazyColumn durumunu LazyColumn konumundan ConversationScreen konumuna taşıma

Son olarak, composable işlevler şunlardır:

LazyListState'in ConversationScreen'e yükseltildiği sohbet oluşturulabilir ağacı
Şekil 4. LazyListState öğesinin ConversationScreen
öğesine yükseltildiği sohbet composable ağacı

Kod 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 düşük ortak üst öğe ConversationScreen&#39;dir.
Şekil 5. LazyListState için en yakın ortak üst öğe ConversationScreen

Durum 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&#39;e yükseltilen durum, Composition dışında saklanır.
Şekil 6. 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:

Kullanıcı, grup sohbetinde &quot;@&quot; yazıp ipucu girdiğinde kullanıcı önerilerini gösteren özellik
Şekil 7. Kullanıcı @ yazıp ipucu girdiğinde grup sohbetinde kullanıcı önerilerini gösteren özellik

Bu ö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