State ve Jetpack Compose

Bir uygulamadaki durum, zaman içinde değişebilen herhangi bir değerdir. Bu çok geniş bir tanımdır ve Room veritabanından sınıftaki bir değişkene kadar her şeyi kapsar.

Tüm Android uygulamaları, durumu kullanıcıya gösterir. Android uygulamalarındaki duruma ilişkin birkaç örnek:

  • Ağ bağlantısı kurulamadığında gösterilen bir Snackbar.
  • Bir blog yayını ve ilişkili yorumlar.
  • Kullanıcı tıkladığında oynatılan düğmelerde dalgalanma animasyonları.
  • Kullanıcının bir resmin üzerine çizebileceği çıkartmalar.

Jetpack Compose, Android uygulamasında durumu nerede ve nasıl depolayıp kullanacağınız konusunda net olmanıza yardımcı olur. Bu kılavuzda, durum ile composable'lar arasındaki bağlantı ve Jetpack Compose'un durumla daha kolay çalışmak için sunduğu API'ler üzerinde durulmaktadır.

Durum ve kompozisyon

Compose, bildirimseldir ve bu nedenle yalnızca aynı composable'ı yeni bağımsız değişkenlerle çağırarak güncellenebilir. Bu bağımsız değişkenler, kullanıcı arayüzü durumunun temsilleridir. Bir durum her güncellendiğinde yeniden oluşturma işlemi gerçekleşir. Bu nedenle, TextField gibi öğeler, zorunlu XML tabanlı görünümlerde olduğu gibi otomatik olarak güncellenmez. Bir composable'ın buna göre güncellenmesi için yeni durumun açıkça belirtilmesi gerekir.

@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = { },
            label = { Text("Name") }
        )
    }
}

Bu kodu çalıştırıp metin girmeye çalıştığınızda hiçbir şey olmadığını görürsünüz. Bunun nedeni, TextField parametresinin kendiliğinden güncellenmemesi, value parametresi değiştiğinde güncellenmesidir. Bunun nedeni, Oluşturma'da oluşturma ve yeniden oluşturma işlemlerinin işleyiş şeklidir.

İlk beste ve yeniden beste hakkında daha fazla bilgi edinmek için Thinking in Compose başlıklı makaleyi inceleyin.

Composable'larda durum

Birleştirilebilir işlevler, bir nesneyi bellekte depolamak için remember API'sini kullanabilir. remember tarafından hesaplanan bir değer, ilk bileşim sırasında Composition'da depolanır ve depolanan değer, yeniden bileşim sırasında döndürülür. remember, hem değiştirilebilir hem de değiştirilemez nesneleri depolamak için kullanılabilir.

mutableStateOf gözlemlenebilir bir tür olan MutableState<T> gözlemlenebilirini oluşturur. Bu tür, Compose çalışma zamanıyla entegre edilmiştir.

interface MutableState<T> : State<T> {
    override var value: T
}

value programlarında yapılan tüm değişiklikler, value okuyan tüm composable işlevlerin yeniden oluşturulmasına neden olur.

Bir composable'da MutableState nesnesi bildirmenin üç yolu vardır:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

Bu bildirimler eşdeğerdir ve durumun farklı kullanımları için söz dizimi kolaylığı olarak sağlanır. Yazdığınız composable'da en kolay okunabilen kodu üreteni seçmelisiniz.

by temsilci söz dizimi için aşağıdaki içe aktarmalar gerekir:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Hatırlanan değeri diğer composable'lar için parametre olarak veya hangi composable'ların görüntüleneceğini değiştirmek üzere ifadelerde mantık olarak bile kullanabilirsiniz. Örneğin, ad boşsa selamlama mesajının gösterilmesini istemiyorsanız durumu bir if ifadesinde kullanın:

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

remember, yeniden oluşturma işlemleri arasında durumu korumanıza yardımcı olsa da yapılandırma değişiklikleri arasında durum korunmaz. Bunun için rememberSaveable uygulamasını kullanmanız gerekir. rememberSaveable, Bundle içinde kaydedilebilecek tüm değerleri otomatik olarak kaydeder. Diğer değerler için özel bir kaydedici nesnesi iletebilirsiniz.

Desteklenen diğer eyalet türleri

Compose, durumu tutmak için MutableState<T> kullanmanızı gerektirmez. Diğer gözlemlenebilir türleri destekler. Compose'da başka bir gözlemlenebilir türü okumadan önce, durumu değiştiğinde composable'ların otomatik olarak yeniden oluşturulabilmesi için bu türü State<T> türüne dönüştürmeniz gerekir.

Compose, Android uygulamalarında kullanılan yaygın gözlemlenebilir türlerden State<T> oluşturma işlevleriyle birlikte gelir. Bu entegrasyonları kullanmadan önce, aşağıda belirtildiği gibi uygun yapıları ekleyin:

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle(), yaşam döngüsüne duyarlı bir şekilde Flow değerlerini toplar ve uygulamanızın uygulama kaynaklarını korumasını sağlar. Compose State'dan yayılan en son değeri temsil eder. Android uygulamalarında akış toplamak için bu API'yi kullanmanız önerilir.

    build.gradle dosyasında şu bağımlılık gereklidir (2.6.0-beta01 veya daha yeni bir sürüm olmalıdır):

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
  • Flow: collectAsState()

    collectAsState, collectAsStateWithLifecycle ile benzerdir. Çünkü Flow değerlerini toplar ve bunları Compose State'a dönüştürür.

    Platformdan bağımsız kod için yalnızca Android'e özel olan collectAsStateWithLifecycle yerine collectAsState kullanın.

    collectAsState, compose-runtime içinde kullanılabildiğinden ek bağımlılıklar gerekmez.

  • LiveData: observeAsState()

    observeAsState() bu LiveData değerini gözlemlemeye başlar ve değerlerini State üzerinden gösterir.

    build.gradle dosyasında aşağıdaki bağımlılık gereklidir:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.8.1"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.8.1"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.8.1"
}

Durum bilgili ve durum bilgisiz karşılaştırması

Bir nesneyi depolamak için remember kullanan bir composable, dahili durum oluşturarak composable'ı durumlu hale getirir. HelloContent, name durumunu dahili olarak tuttuğu ve değiştirdiği için durum bilgisi olan bir composable örneğidir. Bu, arayan kişinin durumu kontrol etmesi gerekmediği ve durumu kendisinin yönetmesi gerekmeden kullanabileceği durumlarda yararlı olabilir. Ancak, dahili durumu olan composable işlevler daha az yeniden kullanılabilir ve test edilmesi daha zordur.

Durumsuz bir composable, herhangi bir durumu tutmayan composable'dır. Durum bilgisizliği elde etmenin kolay bir yolu durum yükseltme kullanmaktır.

Yeniden kullanılabilir composable işlevler geliştirirken genellikle aynı composable işlevin hem durumlu hem de durumsuz bir sürümünü kullanıma sunmak istersiniz. Durumlu sürüm, durumla ilgilenmeyen arayanlar için uygundur. Durumsuz sürüm ise durumu kontrol etmesi veya yükseltmesi gereken arayanlar için gereklidir.

State hoisting

Compose'da durum yükseltme, durumun composable'ı durum bilgisiz hale getirmek için composable'ın çağırana taşınmasıdır. Jetpack Compose'da durum yükseltme için genel kalıp, durum değişkenini iki parametreyle değiştirmektir:

  • value: T: Gösterilecek mevcut değer
  • onValueChange: (T) -> Unit: Değerin değişmesini isteyen bir etkinlik. Burada T, önerilen yeni değerdir.

Ancak onValueChange ile sınırlı değilsiniz. Birleştirilebilir için daha spesifik etkinlikler uygunsa bunları lambda'lar kullanarak tanımlamanız gerekir.

Bu şekilde yükseltilen durumun bazı önemli özellikleri vardır:

  • Tek ve doğru kaynak: Durumu kopyalamak yerine taşıyarak tek bir doğru kaynak olmasını sağlıyoruz. Bu, hataları önlemeye yardımcı olur.
  • Kapsüllenmiş: Yalnızca durum bilgisi olan composable'lar durumlarını değiştirebilir. Tamamen dahili bir birimdir.
  • Paylaşılabilir: Yükseltilmiş durum birden fazla composable ile paylaşılabilir. name öğesini farklı bir composable'da okumak istiyorsanız hoisting bunu yapmanıza olanak tanır.
  • Araya girilebilir: Durumsuz composable'ları arayanlar, durumu değiştirmeden önce etkinlikleri yoksayabilir veya değiştirebilir.
  • Ayrılmış: Durumsuz composable'ların durumu herhangi bir yerde depolanabilir. Örneğin, artık name öğesini ViewModel içine taşıyabilirsiniz.

Örnek durumda, name ve onValueChange öğelerini HelloContent öğesinden çıkarıp ağaçta HelloContent öğesini çağıran bir HelloScreen composable'a taşırsınız.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
    }
}

Durumu HelloContent'dan dışarı taşıyarak composable hakkında akıl yürütmek, onu farklı durumlarda yeniden kullanmak ve test etmek daha kolay olur. HelloContent, durumunun nasıl depolandığından bağımsızdır. Ayrıştırma, HelloScreen öğesini değiştirirseniz veya yerine başka bir öğe koyarsanız HelloContent öğesinin uygulanma şeklini değiştirmeniz gerekmediği anlamına gelir.

Durumun azaldığı ve etkinliklerin arttığı bu kalıba tek yönlü veri akışı adı verilir. Bu durumda, durum HelloScreen'dan HelloContent'ye düşer ve etkinlikler HelloContent'den HelloScreen'ya yükselir. 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.

Daha fazla bilgi edinmek için Durumu nereye taşıyabilirim? sayfasını inceleyin.

Compose'da durumu geri yükleme

rememberSaveable API, remember'ye benzer şekilde çalışır. Çünkü yeniden oluşturma işlemlerinde ve ayrıca kaydedilmiş örnek durumu mekanizması kullanılarak etkinlik veya süreç yeniden oluşturma işlemlerinde durumu korur. Örneğin, bu durum ekran döndürüldüğünde yaşanır.

Durumu depolama yöntemleri

Bundle'ya eklenen tüm veri türleri otomatik olarak kaydedilir. Bundle'ya eklenemeyen bir öğeyi kaydetmek istiyorsanız birkaç seçeneğiniz vardır.

Parcelize

En basit çözüm, nesneye @Parcelize notunu eklemektir. Nesne paketlenebilir hale gelir ve paketlenebilir. Örneğin, bu kod, paketlenebilir City veri türü oluşturur ve durumu kaydeder.

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

MapSaver

@Parcelize herhangi bir nedenle uygun değilse bir nesneyi, sistemin Bundle öğesine kaydedebileceği bir değerler kümesine dönüştürmek için kendi kuralınızı tanımlamak üzere mapSaver öğesini kullanabilirsiniz.

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

ListSaver

Harita için anahtarları tanımlamanıza gerek kalmaması için listSaver öğesini de kullanabilir ve dizinlerini anahtar olarak kullanabilirsiniz:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

Compose'da durum bilgisi depolayıcılar

Basit durum yükseltme, composable işlevlerin kendisinde yönetilebilir. Ancak izlenecek durum miktarı artarsa veya composable işlevlerde gerçekleştirilecek mantık ortaya çıkarsa mantık ve durum sorumluluklarını diğer sınıflara (durum tutucular) devretmek iyi bir uygulamadır.

Daha fazla bilgi edinmek için Compose'da durum yükseltme belgelerine veya daha genel olarak mimari kılavuzundaki Durum tutucular ve kullanıcı arayüzü durumu sayfasına bakın.

Anahtarlar değiştiğinde hatırlatma hesaplamalarını yeniden tetikleme

remember API'si genellikle MutableState ile birlikte kullanılır:

var name by remember { mutableStateOf("") }

Burada, remember işlevinin kullanılması MutableState değerinin yeniden oluşturmalarda korunmasını sağlar.

Genel olarak remember, calculation lambda parametresini alır. remember ilk kez çalıştırıldığında calculation lambda'sını çağırır ve sonucunu depolar. Yeniden oluşturma sırasında remember, en son depolanan değeri döndürür.

Önbelleğe alma durumunun yanı sıra, başlatılması veya hesaplanması maliyetli olan herhangi bir nesneyi ya da bir işlemin sonucunu Composition'da depolamak için remember öğesini de kullanabilirsiniz. Bu hesaplamayı her yeniden oluşturmada tekrarlamak istemeyebilirsiniz. Örneğin, pahalı bir işlem olan şu ShaderBrush nesnesini oluşturmak:

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

remember, değer Kompozisyon'dan ayrılana kadar değeri saklar. Ancak, önbelleğe alınmış değeri geçersiz kılmanın bir yolu vardır. remember API'si key veya keys parametresini de alır. Bu anahtarlardan herhangi biri değişirse işlev bir sonraki remember yeniden oluşturulduğunda önbelleği geçersiz kılar ve hesaplama lambda bloğunu tekrar yürütür. Bu mekanizma, Composition'daki bir nesnenin kullanım ömrü üzerinde kontrol sahibi olmanızı sağlar. Hesaplama, hatırlanan değer Kompozisyon'dan ayrılana kadar değil, girişler değişene kadar geçerli kalır.

Bu mekanizmanın nasıl çalıştığı aşağıdaki örneklerde gösterilmektedir.

Bu snippet'te, Box composable'ının arka plan boyası olarak kullanılan bir ShaderBrush oluşturuluyor. remember, daha önce açıklandığı gibi yeniden oluşturmak maliyetli olduğundan ShaderBrush örneğini depolar. remember, seçilen arka plan resmi olan avatarRes değerini key1 parametresi olarak alır. avatarRes değişirse fırça yeni resimle yeniden oluşturulur ve avatarRes'ya yeniden uygulanır.Box Bu durum, kullanıcı bir seçiciden arka plan olarak başka bir resim seçtiğinde ortaya çıkabilir.

@Composable
private fun BackgroundBanner(
    @DrawableRes avatarRes: Int,
    modifier: Modifier = Modifier,
    res: Resources = LocalContext.current.resources
) {
    val brush = remember(key1 = avatarRes) {
        ShaderBrush(
            BitmapShader(
                ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT
            )
        )
    }

    Box(
        modifier = modifier.background(brush)
    ) {
        /* ... */
    }
}

Aşağıdaki snippet'te durum, düz durum tutucu sınıfına yükseltilir MyAppState. rememberMyAppState işlevi, remember kullanarak sınıfın bir örneğini başlatmak için kullanılır. Bu tür işlevleri, yeniden oluşturma işlemlerinden sonra da varlığını sürdürecek bir örnek oluşturmak için kullanmak, Compose'da yaygın bir yöntemdir. rememberMyAppState işlevi, remember için key parametresi olarak kullanılan windowSizeClass değerini alır. Bu parametre değişirse uygulamanın, düz durum tutucu sınıfını en son değerle yeniden oluşturması gerekir. Örneğin, kullanıcının cihazı döndürmesi bu duruma neden olabilir.

@Composable
private fun rememberMyAppState(
    windowSizeClass: WindowSizeClass
): MyAppState {
    return remember(windowSizeClass) {
        MyAppState(windowSizeClass)
    }
}

@Stable
class MyAppState(
    private val windowSizeClass: WindowSizeClass
) { /* ... */ }

Compose, bir anahtarın değişip değişmediğine karar vermek ve depolanan değeri geçersiz kılmak için sınıfın equals uygulamasını kullanır.

Yeniden oluşturmanın ötesinde anahtarlarla durumu saklama

rememberSaveable API, remember etrafında bir sarmalayıcıdır ve verileri Bundle içinde saklayabilir. Bu API, durumun yalnızca yeniden oluşturma sırasında değil, etkinlik yeniden oluşturma ve sistem tarafından başlatılan işlem sonlandırma sırasında da korunmasını sağlar. rememberSaveable, remember'nin keys aldığı parametreleri aynı amaçla alır.input Girişlerden herhangi biri değiştiğinde önbellek geçersiz kılınır. İşlev bir sonraki sefer yeniden oluşturulduğunda rememberSaveable hesaplama lambda bloğunu yeniden yürütür.

Aşağıdaki örnekte, rememberSaveable, userTypedQuery değerini typedQuery değişene kadar saklar:

var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

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

Bloglar