Compose kullanıcı arayüzü mimarisi

Compose'da kullanıcı arayüzü sabittir. Çizildikten sonra güncellenemez. Kontrol edebileceğiniz şey, kullanıcı arayüzünüzün durumudur. Kullanıcı arayüzünün durumu her değiştiğinde Compose, değişen kullanıcı arayüzü ağacının bölümlerini yeniden oluşturur. Composable'lar durumu kabul edebilir ve etkinlikleri kullanıma sunabilir. Örneğin, bir TextField değeri kabul eder ve geri çağırma işleyicisinden değeri değiştirmesini isteyen bir geri çağırma onValueChange kullanıma sunar.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

Composable'lar durumu kabul edip etkinlikleri kullanıma sunduğundan tek yönlü veri akışı kalıbı, Jetpack Compose ile iyi uyum sağlar. Bu kılavuzda, Compose'da tek yönlü veri akışı kalıbının nasıl uygulanacağı, etkinliklerin ve durum tutucuların nasıl uygulanacağı ve Compose'da ViewModel'lerle nasıl çalışılacağı ele alınmaktadır.

Tek yönlü veri akışı

Tek yönlü veri akışı (UDF), durumun aşağı, etkinliklerin ise yukarı doğru aktığı bir tasarım kalıbıdır. Tek yönlü veri akışını izleyerek, kullanıcı arayüzünde durumu gösteren composable'ları, uygulamanızın durumu depolayan ve değiştiren bölümlerinden ayırabilirsiniz.

Tek yönlü veri akışı kullanan bir uygulamanın kullanıcı arayüzü güncelleme döngüsü şu şekildedir:

  1. Etkinlik: Kullanıcı arayüzünün bir bölümü etkinlik oluşturup bunu yukarıya iletir. Örneğin, işlenmesi için ViewModel'e iletilen bir düğme tıklaması veya kullanıcı oturumunun süresinin dolduğunu belirten bir etkinlik, uygulamanızın diğer katmanlarından iletilir.
  2. Güncelleme durumu: Bir etkinlik işleyici durumu değiştirebilir.
  3. Görüntüleme durumu: Durum tutucu, durumu geçirir ve kullanıcı arayüzü bunu görüntüler.
Etkinlikler kullanıcı arayüzünden durum tutucuya, durum ise durum tutucudan kullanıcı arayüzüne akar.
Şekil 1. Tek yönlü veri akışı.

Jetpack Compose kullanırken bu kalıba uymak çeşitli avantajlar sağlar:

  • Test edilebilirlik: Durumun, onu görüntüleyen kullanıcı arayüzünden ayrılması, her ikisinin de ayrı ayrı test edilmesini kolaylaştırır.
  • Durum kapsülleme: Durum yalnızca tek bir yerde güncellenebildiği ve composable'ın durumu için tek bir doğru kaynak olduğu için tutarsız durumlardan kaynaklanan hatalar oluşturma olasılığınız daha düşüktür.
  • Kullanıcı arayüzü tutarlılığı: Tüm durum güncellemeleri, StateFlow veya LiveData gibi gözlemlenebilir durum tutucular kullanılarak kullanıcı arayüzüne anında yansıtılır.

Jetpack Compose'da tek yönlü veri akışı

Composable işlevler, duruma ve etkinliklere göre çalışır. Örneğin, bir TextField yalnızca value parametresi güncellendiğinde güncellenir ve değerin yeni bir değerle değiştirilmesini isteyen bir etkinlik olan onValueChange geri çağırmasını kullanıma sunar. Compose, State nesnesini değer tutucu olarak tanımlar ve durum değerinde yapılan değişiklikler yeniden oluşturmayı tetikler. Değeri ne kadar süre hatırlamanız gerektiğine bağlı olarak durumu remember { mutableStateOf(value) } veya rememberSaveable { mutableStateOf(value) içinde tutabilirsiniz.

TextField composable'ın değerinin türü String olduğundan bu değer, sabit kodlanmış bir değerden, ViewModel'den veya üst composable'dan iletilerek herhangi bir yerden gelebilir. Bu değeri bir State nesnesinde tutmanız gerekmez ancak onValueChange çağrıldığında değeri güncellemeniz gerekir.

Birleştirilebilir parametreleri tanımlama

Bir composable'ın durum parametrelerini tanımlarken aşağıdaki soruları göz önünde bulundurun:

  • Composable işlevi ne kadar yeniden kullanılabilir veya esnek?
  • Durum parametreleri bu composable'ın performansını nasıl etkiler?

Ayrıştırmayı ve yeniden kullanımı teşvik etmek için her birleştirilebilir öğe mümkün olduğunca az bilgi içermelidir. Örneğin, bir haber makalesinin başlığını tutacak bir composable oluştururken tüm haber makalesini iletmek yerine yalnızca gösterilmesi gereken bilgileri iletmeyi tercih edin:

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

Bazen, tek tek parametrelerin kullanılması da performansı artırır. Örneğin, News, title ve subtitle'den daha fazla bilgi içeriyorsa News'nın yeni bir örneği Header(news)'ye her geçirildiğinde, title ve subtitle değişmemiş olsa bile composable yeniden oluşturulur.

İlettiğiniz parametre sayısını dikkatlice değerlendirin. Çok fazla parametreye sahip bir işlevin ergonomisi azalır. Bu nedenle, bu durumda parametreleri bir sınıfta gruplandırmak tercih edilir.

Oluşturma modundaki etkinlikler

Uygulamanıza yapılan her giriş bir etkinlik olarak gösterilmelidir: dokunmalar, metin değişiklikleri ve hatta zamanlayıcılar veya diğer güncellemeler. Bu etkinlikler kullanıcı arayüzünüzün durumunu değiştirdiğinden ViewModel bunları işlemeli ve kullanıcı arayüzü durumunu güncellemelidir.

Kullanıcı arayüzü katmanı, uygulamanızda tutarsızlıklara ve hatalara yol açabileceğinden hiçbir zaman bir etkinlik işleyicinin dışında durum değiştirmemelidir.

Durum ve etkinlik işleyici lambda'ları için değişmez değerler iletmeyi tercih edin. Bu yaklaşımın sağladığı avantajlar şunlardır:

  • Yeniden kullanılabilirliği artırabilirsiniz.
  • Kullanıcı arayüzünüzün durumun değerini doğrudan değiştirmediğini doğrulayın.
  • Durumun başka bir iş parçacığından değiştirilmediğinden emin olarak eşzamanlılık sorunlarını önlersiniz.
  • Genellikle kod karmaşıklığını azaltırsınız.

Örneğin, parametre olarak String ve lambda kabul eden bir composable, birçok bağlamdan çağrılabilir ve yüksek oranda yeniden kullanılabilir. Uygulamanızdaki üst uygulama çubuğunda her zaman metin gösterildiğini ve bir geri düğmesi olduğunu varsayalım. Metni ve geri düğmesi tutamacını parametre olarak alan daha genel bir MyAppTopAppBar composable tanımlayabilirsiniz:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModel'ler, durumlar ve etkinlikler: bir örnek

Aşağıdakilerden biri doğruysa ViewModel ve mutableStateOf kullanarak uygulamanızda tek yönlü veri akışı da sağlayabilirsiniz:

  • Kullanıcı arayüzünüzün durumu, StateFlow veya LiveData gibi gözlemlenebilir durum tutucular kullanılarak gösterilir.
  • ViewModel, kullanıcı arayüzünden veya uygulamanızın diğer katmanlarından gelen etkinlikleri işler ve etkinliklere göre durum tutucuyu günceller.

Örneğin, oturum açma ekranı uygularken Oturum aç düğmesine dokunulduğunda uygulamanızın ilerleme çubuğu ve ağ çağrısı göstermesi gerekir. Giriş başarılı olursa uygulamanız farklı bir ekrana yönlendirilir. Hata durumunda ise uygulama bir Snackbar gösterir. Ekran durumunu ve etkinliği nasıl modelleyeceğiniz aşağıda açıklanmıştır:

Ekranın dört durumu vardır:

  • Oturum açılmadı: Kullanıcı henüz oturum açmadığında.
  • Devam ediyor: Uygulamanız, ağ araması yaparak kullanıcının oturum açmasını sağlamaya çalıştığında.
  • Hata: Oturum açılırken bir hata oluştuğunda.
  • Oturum açmış: Kullanıcı oturum açtığında.

Bu durumları kapalı sınıf olarak modelleyebilirsiniz. ViewModel, durumu State olarak gösterir, başlangıç durumunu ayarlar ve durumu gerektiği gibi günceller. ViewModel, onSignIn() yöntemini kullanıma sunarak oturum açma etkinliğini de işler.

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

Compose, mutableStateOf API'ye ek olarak LiveData, Flow ve Observable için uzantılar sağlar. Bu uzantılar, dinleyici olarak kaydolmak ve değeri durum olarak temsil etmek için kullanılır.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

Daha fazla bilgi

Jetpack Compose'daki mimari hakkında daha fazla bilgi edinmek için aşağıdaki kaynaklara göz atın:

Örnekler