Oluşturma'daki yan etkiler

Yan etki, uygulamanın durumunda, composable işlevin kapsamı dışında gerçekleşen bir değişikliktir. Oluşturulabilir öğelerin yaşam döngüsü ve öngörülemeyen yeniden oluşturmalar, oluşturulabilir öğelerin yeniden oluşturmalarının farklı sıralarda yürütülmesi veya silinebilecek yeniden oluşturmalar gibi özellikler nedeniyle oluşturulabilir öğeler ideal olarak yan etkisiz olmalıdır.

Ancak bazen yan etkiler gerekli olabilir. Ö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 yan etkiler kullanılabilir. Bu işlemler, composable'ın 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 efekt kullanım alanları

Thinking in Compose (Compose'da Düşünme) dokümanında açıklandığı gibi, composable'lar yan etkisiz olmalıdır. Uygulamanın durumunda değişiklik yapmanız gerektiğinde (Durumu yönetme belgelerinde açıklandığı gibi), bu yan etkilerin öngörülebilir bir şekilde yürütülmesi için Effect API'lerini kullanmanız gerekir.

Efektler, Compose'da farklı olanaklar sunduğundan kolayca aşırı kullanılabilir. Bu işlevlerde yaptığınız işlemlerin kullanıcı arayüzüyle ilgili olduğundan ve Durumu yönetme belgelerinde açıklandığı gibi tek yönlü veri akışını bozmadığından emin olun.

LaunchedEffect: Askıya alma işlevlerini bir composable kapsamında çalıştırma

Bir composable'ın ömrü boyunca çalışmak ve askıya alma işlevlerini çağırabilmek için LaunchedEffect composable'ını kullanın. LaunchedEffect, oluşturma işlemine girdiğinde parametre olarak iletilen kod bloğuyla bir eş yordam başlatır. LaunchedEffect, oluşturmadan ayrılırsa eş yordam 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, alfa değerini yapılandırılabilir bir gecikmeyle değiş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 { mutableLongStateOf(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şlevi olan delay işlevini kullanır. Ardından, animateTo kullanarak alfa değerini sıfıra ve tekrar geri animasyonlu olarak değiştirir. Bu işlem, composable'ın kullanım ömrü boyunca tekrarlanır.

rememberCoroutineScope: Oluşturulabilir öğenin dışında bir eş yordam başlatmak için oluşturma işlemini algılayan bir kapsam elde etme

LaunchedEffect, composable işlev olduğundan yalnızca diğer composable işlevlerin içinde kullanılabilir. Bir eş yordamı, composable dışında başlatmak ancak oluşturmadan ayrıldığında otomatik olarak iptal edilecek şekilde kapsamını belirlemek 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ığı oluşturma noktasına bağlı bir CoroutineScope döndüren composable işlevdir. Çağrı, oluşturma işleminden ayrıldığında kapsam iptal edilir.

Önceki örneği takip ederek, 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şirse yeniden başlatılmaması gereken bir efektteki değere referans verme

LaunchedEffect, temel parametrelerden biri değiştiğinde yeniden başlatılır. Ancak bazı durumlarda, efektinizde bir değer yakalamak isteyebilirsiniz. Bu değer değişirse efektin yeniden başlatılmasını istemezsiniz. Bunu yapmak için rememberUpdatedState kullanarak bu değere referans oluşturmanız gerekir. Bu referans, yakalanıp güncellenebilir. Bu yaklaşım, yeniden oluşturulması ve yeniden başlatılması pahalı veya engelleyici olabilecek uzun süreli işlemler 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 bekleyen ve sürenin dolduğunu 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 */
}

Çağrı sitesinin yaşam döngüsüyle eşleşen 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ılmıştır. onTimeout lambda işlevinin her zaman LandingScreen öğesinin yeniden oluşturulduğu en son değeri içerdiğinden emin olmak için onTimeout öğesinin rememberUpdatedState işleviyle sarmalanması gerekir. Kodda döndürülen State, currentOnTimeout efektte kullanılmalıdır.

DisposableEffect: temizlik gerektiren efektler

Anahtarlar değiştiğinde veya composable, oluşturma işleminden ayrıldığında temizlenmesi gereken yan etkiler için DisposableEffect işlevini kullanın. DisposableEffect anahtarları değişirse composable'ın mevcut efektini atması (temizlemesi) ve efekti tekrar çağırarak sıfırlaması gerekir.

Örneğin, bir 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 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 silinir ve yeni lifecycleOwner ile yeniden başlatılır.

DisposableEffect, kod bloğunun son ifadesi olarak bir onDispose ifadesi içermelidir. Aksi takdirde IDE, derleme zamanı hatası gösterir.

SideEffect: Compose durumunu Compose olmayan koda yayınlama

Compose durumunu, Compose 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 oluşturma garanti edilmeden önce bir efekt gerçekleştirmek yanlıştır. Efekti doğrudan bir composable'a yazarken bu durum geçerlidir.

Örneğin, analiz kitaplığınız, sonraki tüm analiz etkinliklerine özel meta veriler (bu örnekte "kullanıcı özellikleri") 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 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: Compose olmayan durumu Compose durumuna dönüştürme

produceState, döndürülen bir State'a değer aktarabilen, oluşturma kapsamlı bir eş yordam başlatır. Oluşturma dışı durumu Oluşturma durumuna dönüştürmek için kullanın. Örneğin, Flow, LiveData veya RxJava gibi abonelik tabanlı harici durumları Oluşturma'ya getirin.

Üretici, produceState oluşturmaya girdiğinde başlatılır ve oluşturmadan çıktığında iptal edilir. Döndürülen State birleştirilir. Aynı değerin ayarlanması yeniden oluşturmayı tetiklemez.

produceState bir eş yordam oluştursa da askıya alınmayan veri kaynaklarını gözlemlemek için de kullanılabilir. Bu kaynağa olan aboneliği kaldırmak için awaitDispose işlevini kullanın.

Aşağıdaki örnekte, ağdan resim 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ürür.

Compose'da, gözlemlenen bir durum nesnesi veya composable girişi her değiştiğinde yeniden oluşturma gerçekleşir. Bir durum nesnesi veya giriş, kullanıcı arayüzünün güncellemesi gerekenden daha sık değişerek gereksiz yeniden oluşturmaya neden olabilir.

Oluşturulabilir bir öğeye girişleriniz yeniden oluşturmanız gerekenden daha sık değişiyorsa derivedStateOf işlevini kullanmanız gerekir. Bu durum genellikle kaydırma konumu gibi sık sık değişen bir şey olduğunda ortaya çıkar ancak composable'ın yalnızca belirli bir eşiği aştığında tepki vermesi gerekir. derivedStateOf, yalnızca ihtiyacınız olduğu kadar güncellenen, gözlemleyebileceğiniz yeni bir Compose durum nesnesi oluşturur. Bu şekilde, Kotlin Flow'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, görünür ilk öğe her değiştiğinde firstVisibleItemIndex de değişir. Kaydırdıkça değer 0, 1, 2, 3, 4, 5 vb. olur. Ancak yeniden oluşturma işleminin yalnızca değer 0 değerinden büyükse gerçekleşmesi gerekir. Güncelleme sıklığındaki bu uyuşmazlık, derivedStateOf için iyi bir kullanım alanı olduğu anlamına gelir.

Yanlış kullanım

İki Compose durum nesnesini birleştirirken "durum türettiğiniz" için derivedStateOf kullanmanız gerektiği yaygın bir yanılgıdır. Ancak aşağıdaki snippet'te gösterildiği gibi, bu tamamen ek yük oluşturur 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 kadar sık güncellenmelidir. Bu nedenle, aşırı yeniden oluşturma gerçekleşmez ve derivedStateOf kullanılması gerekmez.

snapshotFlow: Compose'un State'ini Flow'lara dönüştürme

State<T> nesnelerini soğuk akışa dönüştürmek için snapshotFlow kullanın. snapshotFlow, toplandığında bloğunu çalıştırır ve içinde okunan State nesnelerin sonucunu yayınlar. snapshotFlow bloğunun içinde okunan State nesnelerinden biri değiştiğinde, yeni değer daha önce yayınlanan değere eşit değilse akış, yeni değeri toplayıcısına yayınlar (bu davranış, Flow.distinctUntilChanged davranışına benzer).

Aşağıdaki örnekte, kullanıcı bir listedeki ilk öğeyi kaydırdığında analizlere 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 yararlanabilecek bir Flow'a dönüştürülür.

Efektler yeniden başlatılıyor

Compose'daki LaunchedEffect, produceState veya DisposableEffect gibi bazı efektler, çalışan 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 ortaya çıkabilir:

  • Efektlerin gerektiğinden daha az yeniden başlatılması uygulamanızda hatalara neden olabilir.
  • Yeniden başlatma işleminin gereğinden fazla yapılması verimsizliğe yol açabilir.

Genel bir kural olarak, kodun efekt bloğunda kullanılan değişken ve sabit 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şmesi, efektin yeniden başlatılmasına neden olmamalıysa değişken rememberUpdatedState ile sarmalanmalıdır. Değişken, anahtarsız bir remember ile sarmalandığı için hiç değişmiyorsa değişkeni efekte anahtar olarak iletmeniz gerekmez.

Yukarıda gösterilen DisposableEffect kodunda, efekt, bloğunda kullanılan lifecycleOwner değerini parametre olarak alır. Bunun nedeni, bu değerlerde yapılacak 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 oluşturma sırasında hiç değişmediğinden DisposableEffect anahtarları olarak currentOnStart ve currentOnStop gerekli değildir. lifecycleOwner parametresini iletmezseniz ve bu parametre değişirse HomeScreen yeniden oluşturulur ancak DisposableEffect silinip yeniden başlatılmaz. Bu durum, o andan itibaren yanlış lifecycleOwner kullanılmasına neden olur.

Anahtar olarak sabitler

Çağrı sitesinin yaşam döngüsünü takip etmesi 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 iki kez düşünün ve ihtiyacınız olanın bu olduğundan emin olun.