Oluşturma'daki yan etkiler

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 SideEffectcomposable öğ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.