Eyalet nereye kaldırılır?

Kullanıcı arayüzü durumunu kaldıracağınız bir Compose uygulamasında kullanıcı arayüzü mantığının mı iş mantığının mı bunu gerektirdiğine bağlıdır. Bu belgede bu iki ana senaryo anlatılmıştır.

En iyi uygulama

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

En düşük ortak üst öğe, Bestenin dışında da olabilir. İş mantığıyla ilgili olduğu için ViewModel konumunda durum kaldırma buna örnek gösterilebilir.

Bu sayfada, bu en iyi uygulama ayrıntılı olarak açıklanmakta ve unutulmaması gereken bir uyarı bulunmaktadır.

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 türlerinin ve mantığın tanımları bulunmaktadır.

Kullanıcı arayüzü durumu

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örüntülemeniz gerekendir. Ö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 verileri içerdiği için genellikle hiyerarşinin diğer katmanlarıyla bağlantılıdır.
  • Kullanıcı arayüzü öğesi durumu, oluşturulma şeklini etkileyen kullanıcı arayüzü öğelerine özgü özellikleri ifade eder. Kullanıcı arayüzü öğeleri gösterilebilir veya gizlenmiş olabilir ve belirli bir yazı tipine, yazı tipi boyutuna veya yazı tipi rengine sahip olabilir. Android Görünümleri'nde View, doğası gereği durum bilgili olduğundan, durumunu değiştirmeye veya sorgulamaya yönelik yöntemler sunduğu için bu durumu kendisini yönetir. Bunun bir örneği, metni için TextView sınıfının get ve set yöntemleridir. Jetpack Composer'da durum, composable'ın dışındadır. Hatta bunu composable'ın yakınlarından, çağrı yapan composable işlevine ya da bir durum sahibine bile kaldırabilirsiniz. Scaffold composable için ScaffoldState buna örnek olarak gösterilebilir.

Mantık

Bir uygulamanın mantığı, 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 haber okuyucu uygulamasında bir makaleye yer işareti koymak. Yer işaretini bir dosyaya veya veritabanına kaydetme mantığı genellikle alana ya da veri katmanlarına yerleştirilir. Durum sahibi genellikle bu mantığı, açığa çıkardıkları yöntemleri çağırarak bu katmanlara aktarır.
  • Kullanıcı arayüzü mantığı, ekranda kullanıcı arayüzü durumunun nasıl gösterileceğiyle ilgilidir. Örneğin, kullanıcı bir kategori seçtiğinde doğru arama çubuğu ipucunu alma, listeyi kaydırarak bir listede belirli bir öğeye gitme veya kullanıcı bir düğmeyi tıkladığında belirli bir ekrana gitme mantığını alma.

Kullanıcı arayüzü mantığı

UI mantığı için okuma veya yazma durumu gerektiğinde, kullanıcı yaşam döngüsünü takip ederek durum kapsamını kullanıcı arayüzüne ayarlamanız gerekir. Bunu başarmak için composable bir fonksiyonda durumu doğru düzeyde kaldırmanız gerekir. Alternatif olarak, bu işlemi kullanıcı arayüzü yaşam döngüsü kapsamına alınmış bir düz durum tutucu sınıfında da yapabilirsiniz.

Aşağıda hem çözümlerin hem de hangisinin ne zaman kullanılacağına dair bir açıklama yer almaktadır.

Eyalet sahibi olarak composable'lar

composable'larda kullanıcı arayüzü mantığının ve UI öğesi durumunun olması, durum ve mantık basitse iyi bir yaklaşımdır. Gerektiğinde eyaletinizi composable'a veya yük asansöre bırakabilirsiniz.

Şehir kaldırma gerekli değil

Kaldırım durumu her zaman gerekli değildir. Durum, başka bir composable'ın kontrol etmesi gerekmediğinde bir composable'da dahili olarak tutulabilir. Bu snippet'te, dokunulduğunda genişleyen ve daraltılan bir composable var:

@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 değişkeni, bu kullanıcı arayüzü öğesinin dahili durumudur. Yalnızca bu composable'da okunup değiştirilmiş, ayrıca mantık sadece çok basit. Bu durumda eyaletin kaldırılması fazla fayda sağlamayacağı için dahili bırakabilirsiniz. Böylece composable, genişletilmiş durumun sahibi ve tek bilgi kaynağı haline gelir.

composable'lar içinde kaldırma

Kullanıcı arayüzü öğenizin durumunu diğer composable'larla paylaşmanız ve farklı yerlerde kullanıcı arayüzü mantığını uygulamanız gerekiyorsa bunu kullanıcı arayüzü hiyerarşisinde daha üst sıraya çıkarabilirsiniz. Bu sayede composable'lar tekrar kullanılabilir ve test edilmesi daha kolay olur.

Aşağıdaki örnek, iki işlev uygulayan bir sohbet uygulamasıdır:

  • JumpToBottom düğmesi, mesaj listesini en alta kaydırır. Düğme, liste durumu üzerinde kullanıcı arayüzü mantığını gerçekleştirir.
  • Kullanıcı yeni mesajlar gönderdikten sonra MessagesList listesi en alta kaydırılır. UserInput, liste durumu üzerinde kullanıcı arayüzü mantığı gerçekleştirir.
JumpToBottom düğmesi olan ve sayfayı en alta kaydırıp yeni mesajlar için sohbet uygulaması
Şekil 1. JumpToBottom düğmesi olan ve ekranı kaydırarak yeni mesajlar için ekranı en alta kaydıran Chat uygulaması

composable hiyerarşi aşağıdaki gibidir:

Chat composable ağacı
Şekil 2. Chat composable ağacı

LazyColumn durumu, ileti dizisi ekranına çekilir. Böylece uygulama, kullanıcı arayüzü mantığını gerçekleştirebilir ve bunu gerektiren tüm composable'ların durumunu okuyabilir:

LazyColumn durumunu LazyColumn'dan Conversation Screen'e kaldırma
Şekil 3. LazyColumn eyaleti LazyColumn bölgesinden ConversationScreen bölgesine kaldırılıyor

Son olarak composable'lar:

LazyListState, ConversationScreen'e çekildi
Şekil 4. LazyListState kimlikli Chat'te composable ağaç ConversationScreen konumuna çekildi

Bu 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 gereken kadar yükseğe kaldırılır. composable bir işlevde başlatıldığı için yaşam döngüsünü takip ederek Beste'de depolanır.

lazyListState öğesinin, MessagesList yönteminde varsayılan rememberLazyListState() değeriyle tanımlandığını unutmayın. Bu, Compose'da sık kullanılan bir kalıptır. Komposable'ları yeniden kullanılabilir ve esnek hale getirir. Ardından, composable'ı, uygulamanın durumunu kontrol etmesi gerekmeyen farklı bölümlerinde kullanabilirsiniz. Bu durum genellikle bir composable'ı test ederken veya önizlerken geçerlidir. LazyColumn, durumunu tam olarak bu şekilde tanımlar.

LazyListState için en küçük ortak üst öğe, ConversationScreen&#39;dur
Şekil 5. LazyListState için en küçük ortak üst öğe ConversationScreen

Eyalet sahibi olarak düz eyalet sahibi sınıfı

Bir composable, bir kullanıcı arayüzü öğesinin bir veya birden fazla durum alanını içeren karmaşık bir kullanıcı arayüzü mantığı içerdiğinde, bu sorumluluğu, basit bir eyalet sahibi sınıfı gibi eyalet sahiplerine devretmelidir. Bu, composable'ın mantığını tek başına daha test edilebilir hale getirir ve karmaşıklığını azaltır. Bu yaklaşım, endişelerin ayrılması ilkesini destekler: composable, kullanıcı arayüzü öğelerini yaymaktan sorumludur ve durum tutucu, kullanıcı arayüzü mantığını ve kullanıcı arayüzü öğesinin durumunu içerir.

Düz durum tutucu sınıfları, composable işlevinizi çağıran kullanıcılara pratik işlevler sunar. Böylece bu mantığı kendilerinin yazmak zorunda kalmazlar.

Bu sade sınıflar Bestede oluşturulur ve hatırlanır. composable'ın yaşam döngüsünü takip ettikleri için Oluşturma kitaplığı tarafından sağlanan rememberNavController() veya rememberLazyListState() gibi türleri alabilirler.

Bunun bir örneği, LazyColumn veya LazyRow kullanıcı arayüzü karmaşıklığını kontrol etmek için Compose'da uygulanan LazyListState düz durum tutucu sınıfıdı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 scrollPosition öğesini depolayan LazyColumn durumunu kapsüller. Ayrıca, örneğin belirli bir öğeye kaydırarak kaydırma konumunu değiştirme yöntemleri de ortaya çıkar.

Gördüğünüz gibi bir composable'ın sorumluluklarını artırmak eyalet sahibine yönelik gereksinimi de artırır. Sorumluluklar kullanıcı arayüzü mantığında ya da sadece izlenecek durum miktarında olabilir.

Yaygın olarak kullanılan diğer bir kalıp, uygulamadaki root composable işlevlerinin karmaşıklığını yönetmek için düz durum tutucu sınıfı kullanmaktır. Böyle bir sınıfı, gezinme durumu ve ekran boyutlandırma gibi uygulama düzeyindeki durumu kapsamak için kullanabilirsiniz. Bunun eksiksiz bir açıklamasını Kullanıcı arayüzü mantığı ve durum bilgisi sayfasında bulabilirsiniz.

İş mantığı

composable'lar ve düz durum sahibi sınıfları, kullanıcı arayüzü mantığından ve UI öğesi durumundan sorumluysa bir ekran düzeyinde durum sahibi aşağıdaki görevlerden sorumludur:

  • Genellikle hiyerarşinin iş ve veri katmanları gibi diğer katmanlarına yerleştirilen uygulamanın iş mantığına erişim sağlar.
  • Uygulama verilerinin, ekran kullanıcı arayüzü durumuna dönüşen belirli bir ekranda sunulması için hazırlanması.

Eyalet sahibi olarak ViewModels

AAC ViewModel'in Android geliştirmedeki avantajları, bunları iş mantığına erişim sağlamaya ve uygulama verilerini ekranda sunum için hazırlamaya uygun hale getirir.

Kullanıcı arayüzünün durumunu ViewModel içinde kaldırdığınızda, bunu Beste'nin dışına taşırsınız.

ViewModel&#39;e yüklenen durum, Bestenin dışında depolanır.
Şekil 6. ViewModel öğesine kaydedilen durum, Bestenin dışında depolanır.

ViewModel'ler, Bestenin bir parçası olarak depolanmaz. Bunlar çerçeve tarafından sağlanır ve etkinlik, parça, gezinme grafiği veya gezinme grafiğinin hedefi olabilecek bir ViewModelStoreOwner'e ayarlanır. ViewModel kapsamları hakkında daha fazla bilgi için dokümanları inceleyebilirsiniz.

Bu durumda ViewModel, bilginin kaynağı ve kullanıcı arayüzü durumunun en küçük ortak üst öğesidir.

Ekran kullanıcı arayüzü durumu

Yukarıdaki tanımlara göre, ekran kullanıcı arayüzü durumu iş kuralları uygulanarak oluşturulur. Bundan ekran düzeyinde durum sahibinin sorumlu olduğu düşünülürse bu, ekran kullanıcı arayüzü durumunun genellikle ekran düzeyi durum tutucusunda (bu örnekte ViewModel) kaldırılacağı anlamına gelir.

Bir sohbet uygulamasının ConversationViewModel özelliğini ve bunu değiştirmek için ekran kullanıcı arayüzü durumunu ve 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 kaldırılmış ekran kullanıcı arayüzünü kullanır. İş mantığına erişim sağlamak için ViewModel örneğini ekran düzeyindeki composable'larınıza eklemeniz gerekir.

Aşağıda, ekran düzeyinde composable'da kullanılan bir ViewModel örneği verilmiştir. Burada, composable ConversationScreen(), ViewModel içinde kaldırılan 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 ayrıntılı incelemesi", verilerin birkaç iç içe yerleştirilmiş alt bileşenden okundukları konuma geçirilmesini ifade eder.

Oluştur'da mülk ayrıntılandırma özelliğinin gösterilebileceği tipik bir örnek, ekran düzeyi durum tutucusunu üst düzeye ekleyip durumu ve etkinlikleri alt composable'lara aktarmanızdır. Bu da ayrıca aşırı miktarda composable işlev imzasına neden olabilir.

Etkinlikleri ayrı lambda parametreleri olarak göstermek işlev imzasını aşırı yükleyebilir ancak composable işlev sorumluluklarının görünürlüğünü en üst düzeye çıkarır. Neler yaptığını bir bakışta görebilirsiniz.

Eyaletleri ve etkinlikleri tek bir yerde toplamak için sarmalayıcı sınıfları oluşturmak yerine mülk sondajı tercih edilir. Çünkü bu, composable sorumlulukların görünürlüğünü azaltır. Sarmalayıcı sınıflarına sahip olmadığınızda, composable'lara yalnızca ihtiyaç duydukları parametreleri iletme olasılığınız da artar. Bu, en iyi uygulamadır.

Bu etkinlikler gezinme etkinlikleri olduğunda da 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

Okuması veya yazması gereken bir iş mantığı varsa kullanıcı arayüzü öğesinin durumunu ekran düzeyindeki durum sahibine yükseltebilirsiniz.

Sohbet uygulaması örneğine devam edecek olursak, kullanıcı @ yazıp bir ipucu yazdığında uygulama, grup sohbetinde kullanıcı önerilerini gösterir. Bu öneriler veri katmanından gelir ve kullanıcı önerileri listesi hesaplama mantığı iş mantığı olarak kabul edilir. Özellik şuna benzer:

Grup sohbetinde kullanıcı &quot;@&quot; yazdığında ve ipucu ile kullanıcı önerilerini gösteren özellik
Şekil 7. Grup sohbetinde kullanıcı, @ yazıp bir ipucu yazdığında kullanıcı önerilerini gösteren özellik

Bu özelliği uygulayan ViewModel şöyle 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ünde tüketilir.

Uyarı

Bazı Compose kullanıcı arayüzü öğesi durumları için ViewModel öğesine yükseltme işleminde özel noktalara dikkat edilmesi gerekebilir. Örneğin, Compose kullanıcı arayüzü öğelerinin bazı durum sahipleri, durumu değiştirme yöntemleri gösterir. Bunların bazıları, animasyonları tetikleyen askıya alma işlevleri olabilir. Bu askıya alma işlevleri, bunları Beste kapsamında olmayan bir CoroutineScopedan çağırırsanız istisnalar atabilir.

Uygulama çekmecesinin içeriğinin dinamik olduğunu ve kapatıldıktan sonra veri katmanından içeriği getirip yenilemeniz gerektiğini varsayalım. Durum sahibinden bu öğedeki kullanıcı arayüzünü ve iş mantığını çağırabilmek için çekmece durumunu ViewModel öğesine kaldırmanız gerekir.

Ancak, Oluştur kullanıcı arayüzünden viewModelScope, DrawerState close() yönteminin çağrılması, "MonotonicFrameClock mesajını içeren IllegalStateException türünde bir çalışma zamanı istisnasına neden olur CoroutineContext”.

Bunu düzeltmek için Besteyi kapsama alan 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 Beste kapsamındaki bir eşle 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 başvurun.

Numuneler

Codelab uygulamaları

Videolar