Compose'da durum ömürleri

Jetpack Compose'da composable işlevler genellikle remember işlevini kullanarak durumu tutar. State ve Jetpack Compose bölümünde açıklandığı gibi, hatırlanan değerler yeniden oluşturma işlemleri arasında tekrar kullanılabilir.

remember, değerlerin yeniden oluşturma işlemleri arasında kalıcı olmasını sağlayan bir araç olsa da durumun genellikle bir oluşturma işleminin yaşam süresinden daha uzun süre devam etmesi gerekir. Bu sayfada, remember, retain, rememberSaveable ve rememberSerializable API'leri arasındaki fark, hangi API'nin ne zaman seçileceği ve Compose'da hatırlanan ve saklanan değerlerin yönetimiyle ilgili en iyi uygulamalar açıklanmaktadır.

Doğru kullanım ömrünü seçme

Compose'da, kompozisyonlar ve ötesinde durumu kalıcı hale getirmek için kullanabileceğiniz çeşitli işlevler vardır: remember, retain, rememberSaveable ve rememberSerializable. Bu işlevler kullanım ömrü ve anlam açısından farklılık gösterir ve her biri belirli durum türlerini depolamak için uygundur. Farklar aşağıdaki tabloda özetlenmiştir:

remember

retain

rememberSaveable, rememberSerializable

Değerler yeniden oluşturma işlemlerinden etkilenmez mi?

Değerler, etkinlik yeniden oluşturulduğunda korunur mu?

Her zaman aynı (===) örnek döndürülür.

Eşdeğer (==) bir nesne döndürülür. Bu nesne, seri durumdan çıkarılmış bir kopya olabilir.

Değerler işlem sonlandırmadan etkilenir mi?

Desteklenen veri türleri

Tümü

Etkinlik yok edildiğinde sızdırılacak nesnelere referans vermemelidir.

Serileştirilebilir olmalıdır
(özel bir Saver veya kotlinx.serialization ile)

Kullanım alanları

  • Beste kapsamındaki nesneler
  • Composable'lar için yapılandırma nesneleri
  • Kullanıcı arayüzü doğruluğu kaybolmadan yeniden oluşturulabilecek durum
  • Önbellekler
  • Uzun ömürlü veya "yönetici" nesneler
  • Kullanıcı girişi
  • Metin alanı girişi, kaydırma durumu, açma/kapatma düğmeleri vb. dahil olmak üzere uygulama tarafından yeniden oluşturulamayan durum.

remember

remember, Compose'da durumu depolamanın en yaygın yoludur. remember ilk kez çağrıldığında, verilen hesaplama yürütülür ve hatırlanır. Yani, birleştirilebilir işlev tarafından gelecekte tekrar kullanılmak üzere Compose tarafından depolanır. Bir composable yeniden oluşturulduğunda kodu tekrar yürütülür ancak remember çağrıları, hesaplamayı tekrar yürütmek yerine değerlerini önceki oluşturmadan döndürür.

Bir composable işlevinin her örneğinin, konumsal notlandırma olarak adlandırılan kendi hatırlanan değerler grubu vardır. Hatırlanan değerler, yeniden oluşturma işlemleri arasında kullanılmak üzere not edildiğinde kompozisyon hiyerarşisindeki konumlarına bağlanır. Bir composable farklı konumlarda kullanılıyorsa kompozisyon hiyerarşisindeki her örneğin kendi hatırlanan değerler grubu vardır.

Hatırlanan bir değer artık kullanılmadığında unutulur ve kaydı silinir. Kompozisyon hiyerarşisinden kaldırıldığında (bir değerin key composable veya MovableContent kullanılmadan farklı bir konuma taşınmak üzere kaldırılıp yeniden eklenmesi dahil) ya da farklı key parametrelerle çağrıldığında hatırlanan değerler unutulur.

Mevcut seçenekler arasında remember en kısa kullanım ömrüne sahiptir ve bu sayfada açıklanan dört anımsama işlevi arasında değerleri en erken unutur. Bu nedenle, aşağıdaki durumlarda en iyi sonucu verir:

  • Kaydırma konumu veya animasyon durumu gibi dahili durum nesneleri oluşturma
  • Her yeniden oluşturmada pahalı nesne yeniden oluşturma işleminden kaçınma

Ancak şunlardan kaçınmanız gerekir:

  • Hatırlanan nesneler, Activity yapılandırması değişiklikleri ve sistem tarafından başlatılan işlem sonlandırmaları sırasında unutulduğu için kullanıcı girişlerini remember ile saklamayın.

rememberSaveable ve rememberSerializable

rememberSaveable ve rememberSerializable, remember üzerine kurulur. Bu işlevler, bu kılavuzda ele alınan notlandırma işlevleri arasında en uzun kullanım ömrüne sahiptir. Yeniden oluşturma işlemleri sırasında nesneleri konumsal olarak ezberlemenin yanı sıra, değerleri kaydederek yapılandırma değişiklikleri ve işlem sonlandırma (sistem, uygulamanızın arka planda çalışırken işlemini sonlandırdığında, genellikle ön plandaki uygulamalar için bellek boşaltmak veya kullanıcı uygulamanız çalışırken izinleri iptal ettiğinde) dahil olmak üzere etkinlik yeniden oluşturma işlemleri sırasında geri yüklenebilmelerini de sağlar.

rememberSerializable, rememberSaveable ile aynı şekilde çalışır ancak kotlinx.serialization kitaplığıyla serileştirilebilen karmaşık türlerin kalıcı olmasını otomatik olarak destekler. Türünüz @Serializable ile işaretlenmişse (veya işaretlenebiliyorsa) rememberSerializable'yı, diğer tüm durumlarda ise rememberSaveable'yi seçin.

Bu durum, hem rememberSaveable hem de rememberSerializable'ı metin alanı girişi, kaydırma konumu ve açma/kapatma durumları gibi kullanıcı girişiyle ilişkili durumu depolamak için mükemmel adaylar haline getirir. Kullanıcının yerini kaybetmemesi için bu durumu kaydetmeniz gerekir. Genel olarak, uygulamanızın başka bir kalıcı veri kaynağından (ör. veritabanı) alamadığı durumları not etmek için rememberSaveable veya rememberSerializable kullanmanız gerekir.

rememberSaveable ve rememberSerializable, notlandırılmış değerlerini Bundle içine seri hale getirerek kaydeder. Bunun iki sonucu vardır:

  • Notlandırdığınız değerler şu veri türlerinden biri veya daha fazlasıyla gösterilebilir: Primitifler (Int, Long, Float, Double dahil), String veya bu türlerin herhangi birinin dizileri.
  • Kaydedilen bir değer geri yüklendiğinde, bu değer (==) ile eşit olan yeni bir örnek olur ancak kompozisyonun daha önce kullandığı referans (===) ile aynı olmaz.

kotlinx.serialization kullanmadan daha karmaşık veri türlerini depolamak için nesnenizi desteklenen veri türlerine seri hale getirmek ve seri hale getirilmiş nesnenizi geri dönüştürmek üzere özel bir Saver uygulayabilirsiniz. Compose'un State, List, Map, Set gibi yaygın veri türlerini kutudan çıktığı haliyle anladığını ve bunları sizin adınıza otomatik olarak desteklenen türlere dönüştürdüğünü unutmayın. Aşağıda, Size sınıfı için bir Saver örneği verilmiştir. Size'nın tüm özellikleri listSaver kullanılarak bir listede paketlenerek uygulanır.

data class Size(val x: Int, val y: Int) {
    object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver(
        save = { listOf(it.x, it.y) },
        restore = { Size(it[0], it[1]) }
    )
}

@Composable
fun rememberSize(x: Int, y: Int) {
    rememberSaveable(x, y, saver = Size.Saver) {
        Size(x, y)
    }
}

retain

retain API, değerlerini ne kadar süreyle ezberlediği açısından remember ile rememberSaveable/rememberSerializable arasında yer alır. Elde tutulan değerler, hatırlanan değerlere kıyasla farklı bir yaşam döngüsüne sahip olduğundan farklı şekilde adlandırılır.

Bir değer saklandığında hem konumsal olarak ezberlenir hem de uygulamanın kullanım ömrüne bağlı ayrı bir kullanım ömrü olan ikincil bir veri yapısında kaydedilir. Saklanan bir değer, yapılandırma değişikliklerinden seri hale getirilmeden etkilenmez ancak işlem sonlandırmadan etkilenir. Kompozisyon hiyerarşisi yeniden oluşturulduktan sonra bir değer kullanılmazsa saklanan değer kullanımdan kaldırılır (bu, retain'ın unutulmaya eşdeğeridir).

rememberSaveable'dan daha kısa olan bu yaşam döngüsü karşılığında retain, lambda ifadeleri, akışlar ve bit eşlemler gibi büyük nesneler gibi serileştirilemeyen değerleri kalıcı hale getirebilir. Örneğin, yapılandırma değişikliği sırasında medya oynatmanın kesintiye uğramasını önlemek için retain kullanarak bir medya oynatıcıyı (ör. ExoPlayer) yönetebilirsiniz.

@Composable
fun MediaPlayer() {
    // Use the application context to avoid a memory leak
    val applicationContext = LocalContext.current.applicationContext
    val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() }
    // ...
}

retain - ViewModel

Her iki retain ve ViewModel, temel olarak yapılandırma değişikliklerinde nesne örneklerini kalıcı hale getirme konusunda benzer işlevler sunar. retain veya ViewModel'a ulaşma tercihi, kalıcı hale getirdiğiniz değerin türüne, kapsamının nasıl olması gerektiğine ve ek işlevlere ihtiyacınız olup olmadığına bağlıdır.

ViewModel, genellikle uygulamanızın kullanıcı arayüzü ile veri katmanları arasındaki iletişimi kapsayan nesnelerdir. Bu sayede, mantığı composable işlevlerinizin dışına taşıyarak test edilebilirliği artırabilirsiniz. ViewModel, ViewModelStore içinde tekil nesneler olarak yönetilir ve saklanan değerlerden farklı bir kullanım ömrüne sahiptir. Bir ViewModel, ViewModelStore yok edilene kadar etkin kalmaya devam ederken, içerik kompozisyondan kalıcı olarak kaldırıldığında saklanan değerler kullanımdan kaldırılır (örneğin, yapılandırma değişikliği için bu, kullanıcı arayüzü hiyerarşisi yeniden oluşturulursa ve kompozisyon yeniden oluşturulduktan sonra saklanan değer kullanılmazsa saklanan değerin kullanımdan kaldırılacağı anlamına gelir).

ViewModel ayrıca Dagger ve Hilt ile bağımlılık ekleme için kullanıma hazır entegrasyonlar, SavedState ile entegrasyon ve arka plan görevlerini başlatmak için yerleşik coroutine desteği içerir. Bu nedenle ViewModel, arka plan görevlerini ve ağ isteklerini başlatmak, projenizdeki diğer veri kaynaklarıyla etkileşim kurmak ve isteğe bağlı olarak hem ViewModel içindeki yapılandırma değişikliklerinde korunması hem de işlem sonlandırmadan etkilenmemesi gereken görev açısından kritik kullanıcı arayüzü durumunu yakalayıp kalıcı hale getirmek için ideal bir yerdir.

retain, belirli composable örnekleriyle sınırlı olan ve kardeş composable'lar arasında yeniden kullanılması veya paylaşılması gerekmeyen nesneler için en uygundur. Burada ViewModel, kullanıcı arayüzü durumunu depolamak ve arka plan görevlerini gerçekleştirmek için iyi bir yer olarak işlev görürken retain, kullanıcı arayüzü altyapısı için nesneleri (ör. önbellekler, gösterim izleme ve analizler, AndroidView'lere bağımlılıklar ve Android OS ile etkileşime giren veya ödeme işleyicileri ya da reklamcılık gibi üçüncü taraf kitaplıklarını yöneten diğer nesneler) depolamak için iyi bir adaydır.

Modern Android uygulama mimarisi önerileri dışında özel uygulama mimarisi kalıpları tasarlayan ileri düzey kullanıcılar için: retain, şirket içi "ViewModel benzeri" bir API oluşturmak için de kullanılabilir. Coroutine'ler ve kayıtlı durum desteği kutudan çıktığı gibi sunulmasa da retain, bu özellikler kullanılarak oluşturulan ViewModel benzerlerinin yaşam döngüsü için yapı taşı olarak kullanılabilir. Böyle bir bileşenin nasıl tasarlanacağı bu kılavuzun kapsamı dışındadır.

retain

ViewModel

Kapsam belirleme (Scoping)

Paylaşılan değerler yoktur. Her değer, kompozisyon hiyerarşisinde belirli bir noktada korunur ve bu noktayla ilişkilendirilir. Aynı türü farklı bir konumda tutmak her zaman yeni bir örnek üzerinde işlem yapar.

ViewModel, ViewModelStore içinde tekil öğelerdir.

Yıkım

Kompozisyon hiyerarşisinden kalıcı olarak ayrıldığınızda

ViewModelStore temizlendiğinde veya yok edildiğinde

Ek işlevler

Nesne, bileşim hiyerarşisinde olduğunda veya olmadığında geri çağırmalar alabilir.

Yerleşik coroutineScope, SavedStateHandle desteği Hilt kullanılarak eklenebilir.

Sahibi

RetainedValuesStore

ViewModelStore

Kullanım alanları

  • Kullanıcı arayüzüne özgü değerleri, tek tek composable örneklerinde yerel olarak kalıcı hale getirme
  • Gösterim izleme (muhtemelen RetainedEffect aracılığıyla)
  • Özel bir "ViewModel benzeri" mimari bileşeni tanımlamak için kullanılan yapı taşı
  • Hem kod düzeni hem de test için kullanıcı arayüzü ile veri katmanları arasındaki etkileşimleri ayrı bir sınıfa çıkarma
  • Flow öğelerini State nesnelerine dönüştürme ve yapılandırma değişiklikleriyle kesintiye uğramaması gereken askıya alma işlevlerini çağırma
  • Durumları tüm ekran gibi büyük kullanıcı arayüzü alanlarında paylaşma
  • View ile birlikte çalışabilirlik

retain ile rememberSaveable veya rememberSerializable'ı birleştirin.

Bazen bir nesnenin hem retained hem de rememberSaveable veya rememberSerializable gibi karma bir kullanım ömrü olması gerekir. Bu, nesnenizin ViewModel olması gerektiğinin bir göstergesi olabilir. ViewModel için Kaydedilmiş Durum modülü kılavuzunda açıklandığı gibi, kaydedilmiş durumu destekleyebilir.

retain ve rememberSaveable veya rememberSerializable aynı anda kullanılabilir. Her iki yaşam döngüsünün doğru şekilde birleştirilmesi önemli ölçüde karmaşıklık katar. Bu kalıbı daha gelişmiş ve özel mimari kalıplarının bir parçası olarak ve yalnızca aşağıdaki koşulların tümü geçerliyse kullanmanızı öneririz:

  • Saklanması veya kaydedilmesi gereken değerlerin bir karışımından oluşan bir nesne tanımlıyorsunuz (ör.kullanıcı girişini izleyen bir nesne ve diske yazılamayan bir bellek içi önbellek).
  • Durumunuz, bir composable ile sınırlandırılmış ve ViewModel'nın tekil kapsamı veya kullanım ömrü için uygun değil.

Bu durumların tümü geçerliyse sınıfınızı üç bölüme ayırmanızı öneririz: Kaydedilen veriler, saklanan veriler ve kendi durumu olmayan, durumu buna göre güncellemek için saklanan ve kaydedilen nesnelere yetki veren bir "aracı" nesne. Bu desen aşağıdaki şekli alır:

@Composable
fun rememberAndRetain(): CombinedRememberRetained {
    val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) {
        ExtractedSaveData()
    }
    val retainData = retain { ExtractedRetainData() }
    return remember(saveData, retainData) {
        CombinedRememberRetained(saveData, retainData)
    }
}

@Serializable
data class ExtractedSaveData(
    // All values that should persist process death should be managed by this class.
    var savedData: AnotherSerializableType = defaultValue()
)

class ExtractedRetainData {
    // All values that should be retained should appear in this class.
    // It's possible to manage a CoroutineScope using RetainObserver.
    // See the full sample for details.
    var retainedData = Any()
}

class CombinedRememberRetained(
    private val saveData: ExtractedSaveData,
    private val retainData: ExtractedRetainData,
) {
    fun doAction() {
        // Manipulate the retained and saved state as needed.
    }
}

Durumu kullanım ömrüne göre ayırarak sorumlulukların ve depolamanın ayrımı çok net bir şekilde ortaya konur. Kaydedilen verilerin, saklanan verilerle değiştirilememesi kasıtlıdır. Bu sayede, savedInstanceState paketi zaten yakalandığında ve güncellenemediğinde kaydedilen verilerin güncellenmeye çalışılması önlenir. Ayrıca, Compose'u çağırmadan veya bir Etkinlik yeniden oluşturma simülasyonu yapmadan oluşturucularınızı test ederek yeniden oluşturma senaryolarını test etmenize de olanak tanır.

Bu kalıbın nasıl uygulanabileceğine dair eksiksiz bir örnek için tam örneğe (RetainAndSaveSample.kt) bakın.

Konumsal notlandırma ve uyarlanabilir düzenler

Android uygulamaları; telefonlar, katlanabilir cihazlar, tabletler ve masaüstü bilgisayarlar gibi birçok form faktörünü destekleyebilir. Uygulamaların, uyarlanabilir düzenler kullanarak bu form faktörleri arasında geçiş yapması gerekir. Örneğin, bir tablette çalışan bir uygulama iki sütunlu bir liste ayrıntısı görünümü gösterebilir ancak daha küçük bir telefon ekranında sunulduğunda liste ve ayrıntı sayfası arasında gezinebilir.

Hatırlanan ve saklanan değerler konuma göre belleğe alındığından yalnızca kompozisyon hiyerarşisinde aynı noktada görünürlerse yeniden kullanılırlar. Düzenleriniz farklı form faktörlerine uyum sağladıkça kompozisyon hiyerarşinizin yapısını değiştirebilir ve değerlerin unutulmasına neden olabilir.

ListDetailPaneScaffold ve NavDisplay gibi hazır bileşenler (Jetpack Navigation 3'ten) için bu bir sorun değildir ve durumunuz düzen değişiklikleri boyunca korunur. Form faktörlerine uyum sağlayan özel bileşenlerde, aşağıdakilerden birini yaparak durumun düzen değişikliklerinden etkilenmediğinden emin olun:

  • Durumlu composable'ların, kompozisyon hiyerarşisinde her zaman aynı yerde çağrıldığından emin olun. Düzen mantığını değiştirerek uyarlanabilir düzenler uygulayın. Bunun için, kompozisyon hiyerarşisinde nesneleri yeniden konumlandırmanız gerekmez.
  • Durumlu composable'ları kontrollü bir şekilde taşımak için MovableContent kullanın. MovableContent örnekleri, hatırlanan ve saklanan değerleri eski konumlarından yeni konumlarına taşıyabilir.

Fabrika işlevlerini hatırlama

Compose kullanıcı arayüzleri composable işlevlerden oluşsa da bir kompozisyonun oluşturulması ve düzenlenmesinde birçok nesne kullanılır. Bunun en yaygın örneği, kendi durumunu tanımlayan karmaşık birleştirilebilir nesnelerdir. Örneğin, LazyList, LazyListState kabul eder.

Compose odaklı nesneleri tanımlarken hem kullanım ömrü hem de önemli girişler dahil olmak üzere amaçlanan hatırlama davranışını tanımlamak için bir remember işlevi oluşturmanızı öneririz. Bu sayede durumunuzdaki tüketiciler, oluşturma hiyerarşisinde beklenen şekilde baki kalacak ve geçersiz kılınacak örnekleri güvenle oluşturabilir. Bir composable factory işlevi tanımlarken aşağıdaki yönergelere uyun:

  • İşlev adının önüne remember ekleyin. İsteğe bağlı olarak, işlev uygulaması nesnenin retained olmasına bağlıysa ve API hiçbir zaman remember'nin farklı bir varyasyonuna bağlı olarak gelişmeyecekse bunun yerine retain önekini kullanın.
  • Durum kalıcılığı seçildiyse ve doğru bir Saver uygulaması yazmak mümkünse rememberSaveable veya rememberSerializable öğesini kullanın.
  • Kullanımla alakalı olmayabilecek CompositionLocal'lara göre yan etkilerden veya değerleri başlatmaktan kaçının. Durumunuzun oluşturulduğu yerin, durumunuzun görüntülendiği yer olmayabileceğini unutmayın.

@Composable
fun rememberImageState(
    imageUri: String,
    initialZoom: Float = 1f,
    initialPanX: Int = 0,
    initialPanY: Int = 0
): ImageState {
    return rememberSaveable(imageUri, saver = ImageState.Saver) {
        ImageState(
            imageUri, initialZoom, initialPanX, initialPanY
        )
    }
}

data class ImageState(
    val imageUri: String,
    val zoom: Float,
    val panX: Int,
    val panY: Int
) {
    object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver(
        save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) },
        restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) }
    )
}