Eyalet nereye kaldırılır?

Compose uygulamasında kullanıcı arayüzü durumunu nerede yukarı taşıyacağınız, kullanıcı arayüzü mantığının mı yoksa iş mantığının mı bunu gerektirdiğine bağlıdır. Bu dokümanda bu iki ana senaryo açıklanmaktadır.

En iyi uygulama

Kullanıcı arayüzü durumunu, durumu okuyan ve yazan tüm composable'lar arasındaki en düşük ortak üst öğeye yükseltmeniz gerekir. Durumu, kullanıldığı yere en yakın yerde tutmanız gerekir. Durum sahibinden, durumu değiştirmek için tüketicilere sabit durum ve etkinlikleri gösterin.

En düşük ortak üst öğe, oluşturmanın dışında da olabilir. Örneğin, iş mantığı söz konusu olduğu için durumu bir ViewModel içinde yukarı taşırken.

Bu sayfada, bu en iyi uygulama ayrıntılı olarak açıklanmakta ve dikkat edilmesi gereken bir husus 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

Kullanıcı arayüzü durumu, kullanıcı arayüzünü tanımlayan özelliktir. İki tür kullanıcı arayüzü durumu vardır:

  • Ekran kullanıcı arayüzü durumu, ekranda göstermeniz gerekenlerdir. Ö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, uygulama verilerini içerdiğinden genellikle hiyerarşinin diğer katmanlarıyla bağlantılıdır.
  • Kullanıcı arayüzü öğesi durumu, kullanıcı arayüzü öğelerinin nasıl oluşturulacağını etkileyen özelliklerini ifade eder. Bir kullanıcı arayüzü öğesi gösterilebilir veya gizlenebilir ve belirli bir yazı tipine, yazı tipi boyutuna ya da yazı tipi rengine sahip olabilir. Jetpack Compose'da durum, composable'ın dışındadır ve hatta composable'ın yakın çevresinden çağrılan composable işleve veya durum bilgisi depolayıcıya taşıyabilirsiniz. Scaffold composable'ı için ScaffoldState buna örnek verilebilir.

Mantık

Uygulamadaki mantık, iş mantığı veya kullanıcı arayüzü mantığı olabilir:

  • İş mantığı, uygulama verileri için ürün gereksinimlerinin uygulanmasıdır. Örneğin, kullanıcı düğmeye dokunduğunda bir haber okuyucu uygulamasında bir makaleye yer işareti koyma. Yer işaretini bir dosyaya veya veritabanına kaydetme mantığı genellikle alan adı ya da 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österileceğiyle ilgilidir. Örneğin, kullanıcı bir kategori seçtiğinde doğru arama çubuğu ipucunu alma, listedeki 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, yaşam döngüsünü takip ederek durumu kullanıcı arayüzü kapsamına almanız gerekir. Bunu yapmak için durumu, composable işlevde doğru düzeyde yukarı taşımanız gerekir. Alternatif olarak, kullanıcı arayüzü yaşam döngüsü kapsamına giren düz durum bilgisi depolayıcı sınıfında da bunu yapabilirsiniz.

Aşağıda her iki çözümün açıklaması ve hangisinin ne zaman kullanılacağıyla ilgili bilgi verilmiştir.

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 bulundurmak iyi bir yaklaşımdır. Durumunuzu bir composable'ın içinde bırakabilir veya gerektiğinde yukarı çekebilirsiniz.

State hoisting gerekmez

Durumu yukarı taşımak 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 vardır:

@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 okunup değiştirilir ve uygulanan mantık çok basittir. Bu durumda durumu yukarı taşımak pek fayda sağlamaz. Bu nedenle, durumu dahili olarak bırakabilirsiniz. Böylece bu composable, genişletilmiş durumun sahibi ve tek doğruluk kaynağı olur.

Birleştirilebilir öğelerde kaldırma

Kullanıcı arayüzü öğesi durumunuzu diğer composable'larla paylaşmanız ve farklı yerlerde kullanıcı arayüzü mantığı uygulamanız gerekiyorsa kullanıcı arayüzü hiyerarşisinde daha yukarı taşıyabilirsiniz. Bu sayede, composable'larınız daha yeniden kullanılabilir ve test edilmesi daha kolay hale gelir.

Aşağıdaki örnekte, iki işlevi uygulayan bir sohbet uygulaması gösterilmektedir:

  • JumpToBottom düğmesi, mesaj listesini en alta kaydırır. Düğme, liste durumunda kullanıcı arayüzü mantığını uygular.
  • Kullanıcı yeni mesajlar gönderdikten sonra MessagesList listesi en alta kaydırılır. UserInput, liste durumunda kullanıcı arayüzü mantığını uygular.
En alta git düğmesi bulunan ve yeni mesajlarda en alta kaydırılan sohbet uygulaması
Şekil 1. JumpToBottom düğmesi bulunan ve yeni mesajlarda en alta kaydırılan sohbet uygulaması

Oluşturulabilir hiyerarşi aşağıdaki gibidir:

Sohbet oluşturulabilir ağacı
Şekil 2. Chat composable ağacı

LazyColumn durumu, uygulamanın kullanıcı arayüzü mantığını uygulayabilmesi ve durumu gerektiren 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 öğesinden ConversationScreen öğesine yükseltme

Son olarak, composable'lar şunlardır:

LazyListState'in ConversationScreen'e yükseltildiği Chat composable ağacı
Şekil 4. ConversationScreen
öğesine yükseltilmiş LazyListState ile sohbet oluşturulabilir 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. Bir composable işlevde başlatıldığı için oluşturma işleminde depolanır ve yaşam döngüsünü takip eder.

lazyListState, MessagesList yönteminde varsayılan rememberLazyListState() değeriyle tanımlanır. Bu, Compose'da yaygın bir kalıptır. Bu sayede, composable'lar daha yeniden kullanılabilir ve esnek hale gelir. Ardından, 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'dir.
Şekil 5. LazyListState için en düşük ortak üst öğe: ConversationScreen

Durum sahibi olarak basit durum bilgisi depolayıcı sınıfı

Bir composable, 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 sayede, composable'ın mantığı tek başına daha kolay test edilebilir ve karmaşıklığı azalır. Bu yaklaşım, sorumlulukların ayrılması ilkesini destekler: Kullanıcı arayüzü öğelerini oluşturmaktan composable sorumludur. Durum bilgisi depolayıcı ise 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şlevinizi çağıranlara kullanışlı işlevler sunar. Böylece bu mantığı kendileri yazmak zorunda kalmazlar.

Bu basit sınıflar, oluşturma işleminde oluşturulur ve hatırlanır. Oluşturulabilir öğenin 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'nin 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 scrollPosition değerini depolayan LazyColumn 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ıya olan ihtiyacı artırır. Sorumluluklar, kullanıcı arayüzü mantığında veya yalnızca takip edilecek durum miktarında olabilir.

Sık kullanılan bir diğer kalıp da uygulamadaki kök composable işlevlerin karmaşıklığını yönetmek için basit bir durum bilgisi depolayıcı sınıfı kullanmaktır. Gezinme durumu ve ekran boyutlandırma gibi uygulama düzeyindeki durumları kapsüllemek için bu tür bir sınıf kullanabilirsiniz. Bunun tam açıklamasını Kullanıcı arayüzü mantığı ve durum bilgisi depolayıcı sayfası bölümünde bulabilirsiniz.

İş mantığı

Kullanıcı arayüzü mantığından ve kullanıcı arayüzü öğesi durumundan composable'lar ve basit durum bilgisi depolayıcı sınıfları sorumluysa ekran düzeyinde durum bilgisi depolayıcı aşağıdaki görevlerden sorumludur:

  • Uygulamanın genellikle hiyerarşinin diğer katmanlarında (ör. işletme ve veri katmanları) bulunan iş mantığına erişim sağlar.
  • Uygulama verilerini belirli bir ekranda sunulmak üzere hazırlama. Bu ekran, ekran kullanıcı arayüzü durumu olur.

Durum sahibi olarak ViewModel'ler

Android geliştirmede AAC ViewModel'lerin avantajları, bunları iş mantığına erişim sağlamak ve uygulama verilerini ekranda sunulmak üzere hazırlamak için uygun hale getirir.

Kullanıcı arayüzü durumunu ViewModel içinde yukarı taşıdığınızda, oluşturmanın dışına taşımış olursunuz.

ViewModel'e yükseltilen durum, oluşturmanın dışında depolanır.
Şekil 6. ViewModel öğesine yükseltilen state hoisting, oluşturmanın dışında depolanır.

ViewModel'ler, oluşturmanın bir parçası olarak depolanmaz. Çerçeve tarafından sağlanırlar ve kapsamları 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 belgelere göz atabilirsiniz.

Bu durumda ViewModel, kullanıcı arayüzü durumu için doğru bilgi kaynağı ve en düşük ortak üst öğe olur.

Ekran kullanıcı arayüzü durumu

Yukarıdaki tanımlara göre ekran kullanıcı arayüzü durumu, iş 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) yukarı çekilir.

Bir sohbet uygulamasının ConversationViewModel ve ekran kullanıcı arayüzü durumunu ve bunu değiştirecek etkinlikleri nasıl gösterdiğini 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) { /* ... */ }
}

Oluşturulabilir öğeler, ViewModel içinde yukarı çekilen 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 eklemeniz gerekir.

Aşağıda, ekran düzeyinde bir composable'da kullanılan ViewModel örneği verilmiştir. Burada, ConversationScreen() composable'ı, ViewModel içinde state hoisting edilen 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 inceleme

"Özellik delme", verilerin birkaç iç içe yerleştirilmiş alt bileşenden okunacakları konuma aktarılmasını ifade eder.

Oluştur'da özellik detaylandırmanın görülebileceği tipik bir örnek, ekran düzeyindeki durum bilgisi depolayıcıyı en üst düzeye ekleyip durum ve etkinlikleri alt bileşenlere aktardığınız durumdur. Bu, composable işlev imzalarının aşırı yüklenmesine de neden olabilir.

Etkinlikleri ayrı lambda parametreleri olarak göstermek işlev imzasını aşırı yükleyebilse de composable işlevin sorumluluklarının görünürlüğünü en üst düzeye çıkarır. Ne işe yaradığını bir bakışta anlayabilirsiniz.

Durumu ve etkinlikleri tek bir yerde kapsüllemek için sarmalayıcı sınıflar oluşturmak yerine özellik detaylandırma tercih edilir. Bu sayede, composable sorumluluklarının görünürlüğü azalır. Sarmalayıcı sınıflar kullanmadığınızda, 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 durum okuma işlemini ertelemeyi de seçebilirsiniz. Daha fazla bilgi için performans belgelerini inceleyebilirsiniz.

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 taşıyabilirsiniz.

Sohbet uygulaması örneğine devam edersek kullanıcı @ ve bir ipucu yazdığında uygulama, grup sohbetinde kullanıcı önerilerini gösterir. Bu öneriler veri katmanından gelir ve kullanıcı önerileri listesini hesaplama mantığı, iş mantığı olarak kabul edilir. Özellik şu şekilde görünür:

Kullanıcı &quot;@&quot; ve bir ipucu yazdığında grup sohbetinde kullanıcı önerileri gösteren özellik
Şekil 7. Kullanıcı @ ve bir ipucu yazdığında grup sohbetinde kullanıcı önerilerini gösteren özellik

Bu özelliği uygulayan ViewModel şu şekilde 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ş yazdığında uygulama, suggestions üretmek için iş mantığını çağırır.

suggestions, ekran kullanıcı arayüzü durumudur ve StateFlow öğesinden toplanarak Compose kullanıcı arayüzünden kullanılır.

Caveat

Bazı Compose kullanıcı arayüzü öğesi durumları için ViewModel'e hoisting işlemi yaparken özel hususları dikkate almanız gerekebilir. Örneğin, Compose kullanıcı arayüzü öğelerinin bazı durum bilgisi depolayıcıları, 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şlevlerini, oluşturma kapsamına girmeyen bir CoroutineScope'ten çağırırsanız istisnalar oluşturabilirler.

Uygulama çekmecesinin içeriğinin dinamik olduğunu ve kapatıldıktan sonra veri katmanından getirilip yenilenmesi gerektiğini varsayalım. Durum sahibinden bu öğe üzerinde hem kullanıcı arayüzünü hem de iş mantığını çağırabilmek için çekmece durumunu ViewModel öğesine yükseltmeniz gerekir.

Ancak Compose kullanıcı arayüzünden viewModelScope kullanarak DrawerState'in close() yöntemini çağırmak, "Bu CoroutineContext” içinde MonotonicFrameClock kullanılamıyor" mesajını içeren IllegalStateException türünde bir çalışma zamanı istisnasına neden olur.

Bu sorunu düzeltmek için kapsamı oluşturma ile sınırlı bir CoroutineScope kullanın. CoroutineContext içinde, askıya alma işlevlerinin çalışması için gerekli olan bir MonotonicFrameClock sağlar.

Bu kilitlenmeyi düzeltmek için ViewModel içindeki eş yordamın CoroutineContext değerini, oluşturma kapsamına alınmış bir değerle değiştirin. Şöyle bir şey olabilir:

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

Durum ve Jetpack Compose hakkında daha fazla bilgi edinmek için aşağıdaki ek kaynakları inceleyin.

Örnekler

Codelab uygulamaları

Videolar