Yan etki, uygulamanın durumunda, bir derlenebilir işlevin kapsamı dışında gerçekleşen bir değişikliktir. Birleştirilebilir öğelerin yaşam döngüsü ve öngörülemeyen yeniden oluşturma, birleştirilebilir öğelerin yeniden oluşturulmasını farklı sıralarda yürütme veya reddedilebilir yeniden oluşturma gibi özellikleri nedeniyle, birleştirilebilir öğeler ideal olarak yan etki içermemelidir.
Ancak bazen yan etkiler gereklidir. Örneğin, belirli bir durum koşulu verildiğinde bir bilgi çubuğu gösterme veya başka bir ekrana gitme gibi tek seferlik bir etkinliği tetiklemek için yan etkiler gereklidir. Bu işlemler, derlenebilir öğenin yaşam döngüsünü bilen 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 etki kullanım alanları
Thinking in Compose dokümanında belirtildiği gibi, derlenebilirler yan etki içermemelidir. Uygulamanın durumunda değişiklik yapmanız gerektiğinde (Durum yönetimi dokümanlarında açıklandığı gibi), bu yan etkilerin tahmin edilebilir bir şekilde yürütülmesi için Etki API'lerini kullanmanız gerekir.
Oluşturma'da efektler sayesinde sunulan farklı olanaklar nedeniyle efektler kolayca aşırı kullanılabilir. Bu dosyalarda yaptığınız çalışmaların kullanıcı arayüzüyle ilgili olduğundan ve Durum yönetimi dokümanlarında açıklandığı gibi tek yönlü veri akışını bozmadığından emin olun.
LaunchedEffect
: Bir composable kapsamında askıya alma işlevlerini çalıştırın
Bir composable'ın ömrü boyunca işlem yapmak ve askıya alma işlevlerini çağırma olanağına sahip olmak için LaunchedEffect
composable'ı kullanın. LaunchedEffect
, kompozisyona girdiğinde parametre olarak iletilen kod bloğunu içeren bir coroutine başlatır. LaunchedEffect
kompozisyondan ayrılırsa coroutine iptal edilir. LaunchedEffect
farklı anahtarlarla yeniden derlenirse (aşağıdaki Efektleri Yeniden Başlatma bölümüne bakın) mevcut coroutine iptal edilir ve yeni askıya alma işlevi yeni bir coroutine'de başlatılır.
Örneğin, alfa değerini yapılandırılabilir bir gecikmeyle titreştiren bir animasyon aşağıda 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üreyi beklemek için duraklatma işlevini delay
kullanır. Ardından, animateTo
kullanarak alfa değerini sıfıra ve tekrar sıfıra kadar sırayla animasyonlu olarak değiştirir.
Bu işlem, bileşenin kullanım ömrü boyunca tekrarlanır.
rememberCoroutineScope
: Bir bileşiğin dışında bir coroutine başlatmak için bileşime 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 coroutine'u bir bileşenin dışında başlatmak ancak bileşimden çıktığında otomatik olarak iptal edilecek şekilde kapsamlı hale getirmek için rememberCoroutineScope
kullanın.
Ayrıca, bir veya daha fazla coroutine'ün yaşam döngüsünü manuel olarak kontrol etmeniz gerektiğinde (ör. bir kullanıcı etkinliği gerçekleştiğinde bir animasyonu iptal etmek) rememberCoroutineScope
değerini kullanın.
rememberCoroutineScope
, çağrıldığı kompozisyon noktasına bağlı bir CoroutineScope
döndüren, birleştirilebilir bir işlevdir. Görüşme, kompozisyondan çıktığında kapsam iptal edilir.
Önceki örnekten yola çıkarak, kullanıcı Button
'a dokunduğunda Snackbar
göstermek için şu 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ğeri değişirse yeniden başlatılmaması gereken bir efektteki bir değere referans verme
LaunchedEffect
, temel parametrelerden biri değiştiğinde yeniden başlatılır. Ancak bazı durumlarda, efektinizde bir değeri yakalamak isteyebilirsiniz. Değişmesi durumunda efektin yeniden başlatılmasını istemezsiniz. Bunu yapmak için, yakalanıp güncellenebilecek bu değere referans oluşturmak üzere rememberUpdatedState
kullanılması gerekir. Bu yaklaşım, yeniden oluşturma ve yeniden başlatmanın pahalı veya yasaklayıcı olabileceği uzun süreli işlemler içeren efektler için yararlıdır.
Örneğin, uygulamanızda bir süre sonra kaybolan bir LandingScreen
olduğunu varsayalım. LandingScreen
yeniden derlense bile, bir süre bekleyip sürenin geçtiğini bildiren efektin yeniden başlatılmaması gerekir:
@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 */ }
Arama sitesinin yaşam döngüsüyle eşleşen bir efekt oluşturmak için parametre olarak Unit
veya true
gibi hiç değişmeyen bir sabit iletilir. Yukarıdaki kodda LaunchedEffect(true)
kullanılır. onTimeout
lambda'sının her zaman LandingScreen
ile yeniden oluşturulan en son değeri içerdiğinden emin olmak için onTimeout
'ün rememberUpdatedState
işleviyle sarmalanması gerekir.
Kodda döndürülen State
, currentOnTimeout
efekti kullanmalıdır.
DisposableEffect
: temizlenmesi gereken efektler
Anahtarlar değiştikten sonra temizlenmesi gereken yan etkiler için veya kompozisyondan ayrılan kompozisyonlar için DisposableEffect
kullanın.
DisposableEffect
anahtarları değişirse bileşimin mevcut efektini yok etmesi (temizlemesi) ve efekti tekrar çağırarak sıfırlaması gerekir.
Örneğin, LifecycleObserver
kullanarak Lifecycle
etkinliklerine dayalı analiz etkinlikleri göndermek isteyebilirsiniz.
Oluşturma'da bu etkinlikleri dinlemek için DisposableEffect
kullanarak gözlemciyi kaydedin ve gerektiğinde gözlemcinin kaydını silin.
@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
öğesini lifecycleOwner
öğesine ekler. lifecycleOwner
değişirse efekt atılır ve yeni lifecycleOwner
ile yeniden başlatılır.
DisposableEffect
, kod bloğunun son beyanı olarak bir onDispose
yan tümcesi 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 öğesini kullanın. SideEffect
kullanmak, efektin her başarılı yeniden oluşturma işleminden sonra yürütülmesini sağlar. Öte yandan, başarılı bir yeniden kompozisyon garanti edilmeden önce bir efekti gerçekleştirmek yanlıştır. Bu durum, efekt doğrudan bir kompozisyona yazılırken ortaya çıkar.
Örneğin, analiz kitaplığınız, sonraki tüm analiz etkinliklerine özel meta veriler ("kullanıcı özellikleri" bu örnekte) ekleyerek kullanıcı kitlenizi segmentlere ayırmanıza olanak tanıyabilir. Mevcut kullanıcının kullanıcı türünü analiz kitaplığınıza iletmek için değerini güncellemek üzere SideEffect
kullanın.
@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ışındaki durumu Oluşturma durumuna dönüştürme
produceState
, döndürülen bir State
öğesine değer gönderebilen, kompozisyon kapsamlı bir coroutine başlatır. Oluşturma dışındaki durumu Oluşturma durumuna dönüştürmek için kullanın. Örneğin, Flow
, LiveData
veya RxJava
gibi harici abonelik odaklı durumları Oluşturma'ya dahil edin.
Üretici, produceState
kompozisyona girdiğinde başlatılır ve kompozisyondan çıktığında iptal edilir. Döndürülen State
değeri birleştirilir; aynı değerin ayarlanması yeniden derlemeyi tetiklemez.
produceState
bir coroutine oluştursa da askıya alınmayan veri kaynaklarını gözlemlemek için de kullanılabilir. Söz konusu kaynağa aboneliği kaldırmak için awaitDispose
işlevini kullanın.
Aşağıdaki örnekte, ağdan resim yüklemek için produceState
işlevinin nasıl kullanılacağı gösterilmektedir. loadNetworkImage
birleştirilebilir işlevi, diğer birleştirilebilirlerde 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
Oluşturma işleminde, gözlemlenen durum nesnesi veya birleştirilebilir giriş her değiştiğinde yeniden oluşturma gerçekleşir. Bir durum nesnesi veya giriş, kullanıcı arayüzünün güncellenmesi gerekenden daha sık değişiyor olabilir. Bu da gereksiz yeniden oluşturmaya neden olur.
Bir bileşiğe yaptığınız girişler, yeniden derlemeniz gerekenden daha sık değişiyorsa derivedStateOf
işlevini kullanmalısınız. Bu durum genellikle kaydırma konumu gibi sık sık değişen bir şey olduğunda ortaya çıkar. Ancak, bileşimin yalnızca belirli bir eşiği aştığında buna tepki vermesi gerekir. derivedStateOf
, yalnızca ihtiyacınız olduğu kadar güncellenen yeni bir Oluşturma durumu nesnesi oluşturur. Bu şekilde, Kotlin Flows 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 değiştiğinde firstVisibleItemIndex
değişir. Kaydırırken değer 0
, 1
, 2
, 3
, 4
, 5
vb. olur. Ancak yeniden derlemenin yalnızca değer 0
'ten büyükse yapılması gerekir.
Güncelleme sıklığındaki bu uyuşmazlık, bunun derivedStateOf
için iyi bir kullanım alanı olduğu anlamına gelir.
Yanlış kullanım
Sık yapılan bir hata, iki Compose durum nesnesini birleştirirken "durum türettiğiniz" için derivedStateOf
kullanmanız gerektiğini varsaymaktır. Ancak bu, aşağıdaki snippet'te gösterildiği gibi tamamen ek yüktür ve 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
ile aynı sıklıkta güncellenmelidir. Bu nedenle, fazladan yeniden derleme gerçekleşmez ve derivedStateOf
kullanılması gerekmez.
snapshotFlow
: Compose'ın durumunu akışlara dönüştürme
State<T>
nesnelerini soğuk bir akışa dönüştürmek için snapshotFlow
simgesini kullanın. snapshotFlow
, toplandığında bloğunu çalıştırır ve içinde okunan State
nesnelerinin sonucunu yayınlar. snapshotFlow
bloğunda okunan State
nesnelerinden biri değiştiğinde, yeni değer önceki yayınlanan değere eşit değilse akış, yeni değeri toplayıcısına gönderir (bu davranış Flow.distinctUntilChanged
'e benzer).
Aşağıdaki örnekte, kullanıcı bir listedeki ilk öğenin ötesine kaydırdığında Analytics'e kaydedilen 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 operatörlerinin gücünden yararlanabilecek bir Flow'a dönüştürülür.
Efektleri yeniden başlatma
Oluşturma'daki bazı efektler (ör. LaunchedEffect
, produceState
veya DisposableEffect
) değişken sayıda bağımsız değişken (anahtar) alır. Bu bağımsız değişkenler, çalışan efekti iptal etmek ve yeni anahtarlarla yeni bir efekt başlatmak için kullanılır.
Bu API'lerin tipik biçimi şudur:
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:
- Efektleri olması gerekenden daha az yeniden başlatmak uygulamanızda hatalara neden olabilir.
- Etkileri gerekenden daha fazla yeniden başlatmak verimsiz olabilir.
Genel kural olarak, koddaki efekt bloğunda kullanılan değişken ve değişmez değişkenler, efekt bileşimine 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 olmayacaksa değişken rememberUpdatedState
içine alınmalıdır. Değişken, anahtar içermeyen bir remember
içine sarıldığı için hiçbir zaman değişmiyorsa değişkeni efektin anahtarı olarak iletmeniz gerekmez.
Yukarıda gösterilen DisposableEffect
kodunda, efekt kendi bloğunda kullanılan lifecycleOwner
parametresini alır. Bunun nedeni, bu parametrelerde yapılan herhangi bir değişikliğin efektin yeniden başlatılmasına neden olmasıdır.
@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)
}
}
}
rememberUpdatedState
kullanımı nedeniyle, değerleri Kompozisyon'da hiçbir zaman değişmediğinden currentOnStart
ve currentOnStop
DisposableEffect
anahtarı olarak gerekli değildir. lifecycleOwner
parametresi iletilmezse ve değişirse HomeScreen
yeniden oluşturulur ancak DisposableEffect
kullanımdan kaldırılmaz ve yeniden başlatılmaz. Bu, o noktadan itibaren yanlış lifecycleOwner
kullanıldığı için sorunlara neden olur.
Anahtar olarak sabitler
Çağrı sitesinin yaşam döngüsünü takip etmesini sağlamak için efekt anahtarı olarak true
gibi bir sabit kullanabilirsiniz. Yukarıda gösterilen LaunchedEffect
örneği gibi geçerli kullanım alanları vardır. Ancak bunu yapmadan önce bir kez daha düşünün ve gerçekten ihtiyacınız 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 bölümünde görünümleri kullanma