Yan etki, uygulamanın durumunda composable işlevin kapsamı dışında gerçekleşen bir değişikliktir. Birleştirilebilir işlevlerin yaşam döngüsü ve öngörülemeyen yeniden oluşturma, birleştirilebilir işlevlerin farklı sıralarda yeniden oluşturulması veya atılabilecek yeniden oluşturma gibi özellikleri nedeniyle, birleştirilebilir işlevler ideal olarak yan etkilerden arındırılmış olmalıdır.
Ancak bazen yan etkiler gereklidir. Örneğin, belirli bir durum koşulu verildiğinde snackbar gösterme veya başka bir ekrana gitme gibi tek seferlik bir etkinliği tetiklemek için. Bu işlemler, composable'ın yaşam döngüsünün farkında olan kontrollü bir ortamdan çağrılmalıdır. Bu sayfada, Jetpack Compose'un sunduğu farklı yan etki API'leri hakkında bilgi edineceksiniz.
Durum ve efekt kullanım alanları
Compose'da düşünme dokümanında belirtildiği gibi, composable işlevler yan etkilerden bağımsız olmalıdır. Uygulamanın durumunda değişiklik yapmanız gerektiğinde (Durumu yönetme dokümanında açıklandığı gibi) yan etkilerin tahmin edilebilir bir şekilde yürütülmesi için Effect API'lerini kullanmanız gerekir.
Mesaj Yazma Sihirbazı'nda efektlerin sunduğu farklı olanaklar nedeniyle, bu efektler kolayca aşırı kullanılabilir. Bu bileşenlerde yaptığınız işin kullanıcı arayüzüyle ilgili olduğundan ve Durumu yönetme dokümanında açıklandığı gibi tek yönlü veri akışını bozmadığından emin olun.
LaunchedEffect
: askıya alma işlevlerini composable kapsamında çalıştırma
Bir composable'ın ömrü boyunca işlem yapmak ve askıya alma işlevlerini çağırmak için LaunchedEffect
composable'ını kullanın. LaunchedEffect
, Composition'a girdiğinde parametre olarak iletilen kod bloğuyla birlikte bir coroutine başlatır. LaunchedEffect
, kompozisyondan ayrılırsa coroutine iptal edilir. LaunchedEffect
farklı anahtarlarla yeniden oluşturulursa (aşağıdaki Efektleri Yeniden Başlatma bölümüne bakın) mevcut eş yordam iptal edilir ve yeni askıya alma işlevi yeni bir eş yordamda başlatılır.
Örneğin, burada alfa değerini yapılandırılabilir bir gecikmeyle titreten bir animasyon verilmiştir:
// Allow the pulse rate to be configured, so it can be sped up if the user is running // out of time var pulseRateMs by remember { mutableStateOf(3000L) } val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes while (isActive) { delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user alpha.animateTo(0f) alpha.animateTo(1f) } }
Yukarıdaki kodda, animasyon, belirlenen süre boyunca beklemek için askıya alma işlevini
delay
kullanır. Ardından, animateTo
kullanarak alfa değerini sırayla sıfıra ve tekrar geri animasyonlandırır.
Bu işlem, composable'ın kullanım ömrü boyunca tekrarlanır.
rememberCoroutineScope
: Bir composable dışında bir coroutine başlatmak için kompozisyona duyarlı bir kapsam elde edin
LaunchedEffect
, birleştirilebilir bir işlev olduğundan yalnızca diğer birleştirilebilir işlevlerin içinde kullanılabilir. Bir composable dışında ancak kompozisyondan ayrıldığında otomatik olarak iptal edilecek şekilde kapsamlı bir coroutine başlatmak için rememberCoroutineScope
kullanın.
Ayrıca, bir veya daha fazla eş yordamın yaşam döngüsünü manuel olarak kontrol etmeniz gerektiğinde (ör. bir kullanıcı etkinliği gerçekleştiğinde animasyonu iptal etme) rememberCoroutineScope
kullanın.
rememberCoroutineScope
, çağrıldığı kompozisyon noktasına bağlı bir CoroutineScope
döndüren birleştirilebilir bir işlevdir. Görüşme Composition'dan ayrıldığında kapsam iptal edilir.
Önceki örnekten yola çıkarak, kullanıcı bir Button
öğesine dokunduğunda Snackbar
göstermek için bu kodu kullanabilirsiniz:
@Composable fun MoviesScreen(snackbarHostState: SnackbarHostState) { // Creates a CoroutineScope bound to the MoviesScreen's lifecycle val scope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> Column(Modifier.padding(contentPadding)) { Button( onClick = { // Create a new coroutine in the event handler to show a snackbar scope.launch { snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
rememberUpdatedState
: Değer değiştiğinde yeniden başlatılmaması gereken bir efektteki değere referans verme
LaunchedEffect
, temel parametrelerden biri değiştiğinde yeniden başlar. Ancak bazı durumlarda, efektinizde bir değer yakalamak isteyebilirsiniz. Bu değer değişirse efektin yeniden başlatılmasını istemeyebilirsiniz. Bunu yapmak için, yakalanıp güncellenebilen bu değere referans oluşturmak üzere rememberUpdatedState
kullanılması gerekir. Bu yaklaşım, uzun süren ve yeniden oluşturulması ile yeniden başlatılması pahalı veya yasaklayıcı olabilecek işlemleri içeren efektler için faydalıdır.
Örneğin, uygulamanızda bir süre sonra kaybolan bir LandingScreen
olduğunu varsayalım. LandingScreen
yeniden oluşturulsa bile bir süre bekleyip sürenin geçtiğini bildiren efekt yeniden başlatılmamalıdır:
@Composable fun LandingScreen(onTimeout: () -> Unit) { // This will always refer to the latest onTimeout function that // LandingScreen was recomposed with val currentOnTimeout by rememberUpdatedState(onTimeout) // Create an effect that matches the lifecycle of LandingScreen. // If LandingScreen recomposes, the delay shouldn't start again. LaunchedEffect(true) { delay(SplashWaitTimeMillis) currentOnTimeout() } /* Landing screen content */ }
Çağrı sitesinin yaşam döngüsüne uygun bir efekt oluşturmak için Unit
veya true
gibi hiç değişmeyen bir sabit parametre olarak iletilir. Yukarıdaki kodda LaunchedEffect(true)
kullanılır. onTimeout
lambda'nın LandingScreen
ile yeniden oluşturulan en son değeri her zaman içermesini sağlamak için onTimeout
, rememberUpdatedState
işleviyle sarmalanmalıdır.
Kodda döndürülen State
, currentOnTimeout
, efektte kullanılmalıdır.
DisposableEffect
: Temizleme gerektiren efektler
Tuşlar değiştiğinde veya composable, Composition'dan ayrıldığında temizlenmesi gereken yan etkiler için DisposableEffect
kullanın.
DisposableEffect
anahtarları değişirse composable'ın mevcut efektini kaldırması (temizlemesi) ve efekti tekrar çağırarak sıfırlaması gerekir.
Örneğin, Lifecycle
etkinliklerine dayalı analiz etkinliklerini LifecycleObserver
kullanarak göndermek isteyebilirsiniz.
Oluşturma'da bu etkinlikleri dinlemek için gerektiğinde gözlemciyi kaydetmek ve kaydını silmek üzere DisposableEffect
kullanın.
@Composable fun HomeScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, // Send the 'started' analytics event onStop: () -> Unit // Send the 'stopped' analytics event ) { // Safely update the current lambdas when a new one is provided val currentOnStart by rememberUpdatedState(onStart) val currentOnStop by rememberUpdatedState(onStop) // If `lifecycleOwner` changes, dispose and reset the effect DisposableEffect(lifecycleOwner) { // Create an observer that triggers our remembered callbacks // for sending analytics events val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } // Add the observer to the lifecycle lifecycleOwner.lifecycle.addObserver(observer) // When the effect leaves the Composition, remove the observer onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } /* Home screen content */ }
Yukarıdaki kodda, efekt observer
karakterini lifecycleOwner
karakterine ekler. lifecycleOwner
değişirse efekt kaldırılır ve yeni lifecycleOwner
ile yeniden başlatılır.
Bir DisposableEffect
, kod bloğunun son ifadesi olarak bir onDispose
ifadesi içermelidir. Aksi takdirde, IDE derleme zamanı hatası gösterir.
SideEffect
: Oluşturma durumunu Oluşturma dışı koda yayınlama
Oluşturma durumu, Oluşturma tarafından yönetilmeyen nesnelerle paylaşmak için
SideEffect
composable'ı kullanın. SideEffect
kullanmak, etkinin her başarılı yeniden oluşturma işleminden sonra yürütülmesini garanti eder. Öte yandan, başarılı bir yeniden oluşturma garanti edilmeden önce bir efekt uygulamak yanlıştır. Bu durum, efekti doğrudan bir composable'da yazarken geçerlidir.
Örneğin, analiz kitaplığınız, tüm sonraki analiz etkinliklerine özel meta veriler ("kullanıcı özellikleri" bu örnekte) ekleyerek kullanıcı popülasyonunuzu segmentlere ayırmanıza olanak tanıyabilir. Mevcut kullanıcının kullanıcı türünü analiz kitaplığınıza iletmek için SideEffect
kullanarak değerini güncelleyin.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
produceState
: Oluşturma dışı durumu Oluşturma durumuna dönüştürme
produceState
Composition kapsamlı bir ortak yordam başlatır. Bu ortak yordam, döndürülen State
içine değer gönderebilir. Bu işlevi, Oluşturma dışı durumu Oluşturma durumuna dönüştürmek için kullanın. Örneğin, Flow
, LiveData
veya RxJava
gibi harici abonelik odaklı durumu Composition'a getirmek için kullanabilirsiniz.
produceState
Kompozisyon'a girdiğinde üretici başlatılır ve Kompozisyon'dan çıktığında iptal edilir. Döndürülen State
birleştirir;
aynı değeri ayarlamak yeniden oluşturmayı tetiklemez.
produceState
bir eşzamanlı rutin oluşturmasına rağmen, askıya alınmayan veri kaynaklarını gözlemlemek için de kullanılabilir. Bu kaynağın aboneliğini kaldırmak için awaitDispose
işlevini kullanın.
Aşağıdaki örnekte, ağdan bir resmi yüklemek için produceState
öğesinin nasıl kullanılacağı gösterilmektedir. loadNetworkImage
composable işlevi, diğer composable'larda kullanılabilecek bir State
döndürür.
@Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository = ImageRepository() ): State<Result<Image>> { // Creates a State<T> with Result.Loading as initial value // If either `url` or `imageRepository` changes, the running producer // will cancel and will be re-launched with the new inputs. return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { // In a coroutine, can make suspend calls val image = imageRepository.load(url) // Update State with either an Error or Success result. // This will trigger a recomposition where this State is read value = if (image == null) { Result.Error } else { Result.Success(image) } } }
derivedStateOf
: Bir veya daha fazla durum nesnesini başka bir duruma dönüştürme
Compose'da, gözlemlenen bir durum nesnesi veya composable giriş her değiştiğinde yeniden kompozisyon gerçekleşir. Bir durum nesnesi veya giriş, kullanıcı arayüzünün gerçekten güncellenmesi gerekenden daha sık değişiyor olabilir. Bu durum, gereksiz yeniden oluşturmaya yol açar.
Bir composable'a girişleriniz, yeniden oluşturmanız gerekenden daha sık değiştiğinde derivedStateOf
işlevini kullanmanız gerekir. Bu durum genellikle kaydırma konumu gibi sık sık değişen bir şey olduğunda meydana gelir. Ancak composable'ın belirli bir eşiği aştıktan sonra yalnızca bir kez tepki vermesi gerekir. derivedStateOf
, yalnızca gerektiği kadar güncellenen, gözlemleyebileceğiniz yeni bir Compose durum nesnesi oluşturur. Bu şekilde, Kotlin Flows'un distinctUntilChanged()
operatörüne benzer şekilde çalışır.
Doğru kullanım
Aşağıdaki snippet'te derivedStateOf
için uygun bir kullanım alanı gösterilmektedir:
@Composable // When the messages parameter changes, the MessageList // composable recomposes. derivedStateOf does not // affect this recomposition. fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
Bu snippet'te, ilk görünür öğe her değiştiğinde firstVisibleItemIndex
değişir. Kaydırma işlemi sırasında değer 0
, 1
, 2
, 3
, 4
, 5
vb. olur.
Ancak yeniden oluşturma yalnızca değer 0
'dan büyükse gereklidir.
Güncelleme sıklığındaki bu uyuşmazlık, derivedStateOf
için iyi bir kullanım alanı olduğunu gösterir.
Yanlış kullanım
Sık yapılan bir hata, iki Compose durum nesnesini birleştirirken "durum türetme" işlemi yaptığınız için derivedStateOf
kullanmanız gerektiğini düşünmektir. Ancak bu, aşağıdaki snippet'te gösterildiği gibi tamamen ek yük olup gerekli değildir:
// DO NOT USE. Incorrect usage of derivedStateOf. var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!! val fullNameCorrect = "$firstName $lastName" // This is correct
Bu snippet'te fullName
, firstName
ve lastName
kadar sık güncellenmelidir. Bu nedenle, fazla yeniden oluşturma işlemi gerçekleşmez ve derivedStateOf
kullanılması gerekmez.
snapshotFlow
: Oluşturma'nın durumunu Akışlar'a dönüştürme
snapshotFlow
simgesini kullanarak State<T>
nesnelerini soğuk akışa dönüştürün. Toplandığında snapshotFlow
bloğunu çalıştırır ve içinde okunan State
nesnelerinin sonucunu verir. snapshotFlow
bloğunda okunan State
nesnelerinden biri değiştiğinde, yeni değer daha önce yayınlanan değerle eşit değilse Akış, yeni değeri toplayıcısına yayınlar (bu davranış Flow.distinctUntilChanged
'ın davranışına benzer).
Aşağıdaki örnekte, kullanıcının listedeki ilk öğeyi geçerek kaydırdığı zamanı Analytics'e kaydeden bir yan etki gösterilmektedir:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
Yukarıdaki kodda listState.firstVisibleItemIndex
, Flow'un operatörlerinin gücünden yararlanabilen bir Flow'a dönüştürülür.
Yeniden başlatma efektleri
Oluşturma'daki LaunchedEffect
, produceState
veya DisposableEffect
gibi bazı efektler, çalıştırılan efekti iptal etmek ve yeni anahtarlarla yeni bir efekt başlatmak için kullanılan değişken sayıda bağımsız değişken (anahtar) alır.
Bu API'lerin tipik biçimi şöyledir:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
Bu davranışın incelikleri nedeniyle, efekti yeniden başlatmak için kullanılan parametreler doğru değilse sorunlar oluşabilir:
- Efektlerin gerektiğinden daha az yeniden başlatılması, uygulamanızda hatalara neden olabilir.
- Efektleri gerekenden fazla yeniden başlatmak verimsiz olabilir.
Genel bir kural olarak, kodun efekt bloğunda kullanılan değiştirilebilir ve değiştirilemez değişkenler, efekt composable'ına parametre olarak eklenmelidir. Bunların dışında, efekti yeniden başlatmaya zorlamak için daha fazla parametre eklenebilir. Bir değişkenin değiştirilmesi efektin yeniden başlatılmasına neden olmamalıysa değişken rememberUpdatedState
içine alınmalıdır. Değişken, anahtarı olmayan bir remember
ile sarmalandığı için hiç değişmiyorsa değişkeni efekt için anahtar olarak iletmeniz gerekmez.
Yukarıda gösterilen DisposableEffect
kodunda, efekt parametre olarak kendi bloğunda kullanılan lifecycleOwner
değerini alır. Çünkü bu değerlerdeki herhangi bir değişiklik, efektin yeniden başlamasına neden olur.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
currentOnStart
ve currentOnStop
, DisposableEffect
anahtarları olarak gerekli değildir. Çünkü rememberUpdatedState
kullanımı nedeniyle değerleri Composition'da asla değişmez. lifecycleOwner
parametresini iletmezseniz ve bu parametre değişirse HomeScreen
yeniden oluşturulur ancak DisposableEffect
kaldırılıp yeniden başlatılmaz. Bu durum, o noktadan itibaren yanlış lifecycleOwner
kullanıldığı için sorunlara neden olur.
Anahtar olarak sabitler
true
gibi bir sabiti, çağrı sitesinin yaşam döngüsünü takip etmesi için efekt anahtarı olarak kullanabilirsiniz. LaunchedEffect
Yukarıdaki örnekte gösterildiği gibi, bu özellik için geçerli kullanım alanları vardır. Ancak bunu yapmadan önce iki kez düşünün ve gerçekten ihtiyacınız olan şeyin bu olduğundan emin olun.
Sizin için önerilenler
- Not: JavaScript kapalıyken bağlantı metni gösterilir.
- State ve Jetpack Compose
- Jetpack Compose için Kotlin
- Oluşturma penceresinde görünümleri kullanma