Compose'da durum ömürleri

Jetpack Compose'da composable işlevler genellikle remember işlevini kullanarak durumu korur. 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şturmanın 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 depolamaya 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 örnekleri

  • Beste kapsamına alınan nesneler
  • Oluşturulabilir işlevler için yapılandırma nesneleri
  • Kullanıcı arayüzü doğruluğu kaybolmadan yeniden oluşturulabilecek durum
  • Önbellek
  • 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 önceki oluşturmadaki değerlerini 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şturmalar 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. Hatırlanan değerler, kompozisyon hiyerarşisinden kaldırıldığında (bir değerin kaldırılıp key composable veya MovableContent kullanılmadan farklı bir konuma taşınmak üzere yeniden eklendiği durumlar dahil) ya da farklı key parametrelerle çağrıldığında 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ğıdakiler için en uygun seçenektir:

  • 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:

  • remember ile kullanıcı girişlerini depolamayın. Çünkü hatırlanan nesneler, Etkinlik yapılandırması değişiklikleri ve sistem tarafından başlatılan süreç sonlandırmaları sırasında unutulur.

rememberSaveable ve rememberSerializable

rememberSaveable ve rememberSerializable, remember üzerine kuruludur. 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ışan işlemini genellikle ön plandaki uygulamalar için bellek boşaltmak amacıyla veya kullanıcı, uygulamanız çalışırken izinleri iptal ederse sonlandırır) 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ı hale getirilmesini otomatik olarak destekler. Türünüz @Serializable ile işaretlenmişse (veya işaretlenebilecekse) rememberSerializable, diğer tüm durumlarda ise rememberSaveable simgesini seçin.

Bu nedenle hem rememberSaveable hem de rememberSerializable, metin alanı girişi, kaydırma konumu, açma/kapatma durumları vb. dahil olmak üzere kullanıcı girişiyle ilişkili durumu depolamak için mükemmel adaylardır. 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 halini kaldırmak ü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 Saver örneği verilmiştir. Size'nın tüm özellikleri listSaver kullanılarak bir listeye yerleştirilerek 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ğerlerden 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üne sahip 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, lambda ifadeleri, akışlar ve bit eşlemler gibi büyük nesneler gibi serileştirilemeyen değerler korunabilir. Ö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 ikisi de temel olarak, yapılandırma değişikliklerinde nesne örneklerini kalıcı hale getirme konusunda benzer işlevler sunar.retainViewModel retain veya ViewModel seçeneklerinden hangisini kullanacağınız, kalıcı hale getirdiğiniz değerin türüne, kapsamının nasıl belirlenmesi 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 öğeler 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. ViewModel, kullanıcı arayüzü durumunu depolamak ve arka plan görevlerini gerçekleştirmek için iyi bir yer görevi görürken retain, önbellekler, gösterim izleme ve analizler, AndroidView'ye bağımlılıklar ve Android işletim sistemiyle etkileşime giren veya ödeme işlemcileri ya da reklamcılık gibi üçüncü taraf kitaplıklarını yöneten diğer nesneler gibi kullanıcı arayüzü tesisatı için nesneleri depolamak üzere 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 için destek varsayılan olarak 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ğıyla ilgili ayrıntılar bu kılavuzun kapsamı dışındadır.

retain

ViewModel

Kapsam belirleme

Paylaşılan değer yoktur. Her değer, kompozisyon hiyerarşisinde belirli bir noktada tutulur 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. Bu, 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ü doğru şekilde birleştirmek ö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ğıdakilerin 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, composable kapsamındadır ve ViewModel'nın tekil kapsamı veya yaşam süresi için uygun değildir.

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ılması çok açık hale gelir. Kaydedilen verilerin, saklanan verilerle değiştirilememesi, savedInstanceState paketi yakalandıktan sonra kaydedilen verilerin güncellenmeye çalışıldığı ve güncellenemediği bir senaryoyu önlemek için tasarlanmıştır. 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 dahil olmak üzere 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 kullanıma 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. Bileşim hiyerarşisindeki nesneleri yeniden konumlandırmak yerine düzen mantığını değiştirerek uyarlanabilir düzenler uygulayın.
  • 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 bileşimin 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 temel 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 eyaletinizdeki tüketiciler, kompozisyon hiyerarşisinde beklenen şekilde geçerliliğini koruyacak ve geçersiz kılınacak örnekleri güvenle oluşturabilir. Bir composable fabrika 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ı olacak şekilde 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) }
    )
}