Oluşturma'daki yan etkiler

Yan etki, uygulamanın durumunda composable işlevin kapsamı dışında gerçekleşen bir değişikliktir. composable'ların yaşam döngüsü ve öngörülemeyen yeniden kompozisyonlar gibi özellikleri nedeniyle, composable'ların farklı sıralarda yeniden kompozisyonları yürütme veya silinebilen yeniden kompozisyonlar gibi özellikleri nedeniyle, composable'lar ideal olarak yan etki içermemelidir.

Bununla birlikte, bazen atıştırmalık çubuğu gösterme veya belirli bir durum koşulunda başka bir ekrana gitme gibi tek seferlik bir etkinliği tetiklemek için yan etkiler gerekir. 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 etki kullanım alanlarını belirtin

Thinking in Compose belgelerinde belirtildiği gibi composable'lar yan efekt içermemelidir. Uygulamanın durumunda değişiklik yapmanız gerektiğinde (Durumu yönetme belgeleri dokümanında açıklandığı gibi) bu yan etkilerin tahmin edilebilir bir şekilde yürütülmesi için Efekt API'lerini kullanmanız gerekir.

Oluşturma'da açılan farklı olasılık efektleri nedeniyle, bu efektler kolayca aşırı kullanılabilir. Bunlarda yaptığınız işin kullanıcı arayüzü ile ilgili olduğundan ve Durumu yönetme belgelerinde açıklandığı gibi tek yönlü veri akışını bozmadığından emin olun.

LaunchedEffect: composable kapsamındaki askıya alma işlevlerini çalıştırma

Askıya alma işlevlerini bir composable'ın içinden güvenli bir şekilde çağırmak için LaunchedEffect composable'ı kullanın. LaunchedEffect Beste bölümüne girdiğinde, parametre olarak geçirilen kod bloğuyla bir eş yordam başlatır. LaunchedEffect besteden ayrılırsa eş yordam iptal edilir. LaunchedEffect farklı anahtarlarla yeniden oluşturulursa (aşağıdaki Yeniden Başlatma Efektleri 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, Scaffold içinde bir Snackbar gösterme işlemi askıya alma işlevi olan SnackbarHostState.showSnackbar işleviyle yapılır.

@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    snackbarHostState: SnackbarHostState
) {

    // If the UI state contains an error, show snackbar
    if (state.hasError) {

        // `LaunchedEffect` will cancel and re-launch if
        // `scaffoldState.snackbarHostState` changes
        LaunchedEffect(snackbarHostState) {
            // Show snackbar using a coroutine, when the coroutine is cancelled the
            // snackbar will automatically dismiss. This coroutine will cancel whenever
            // `state.hasError` is false, and only start when `state.hasError` is true
            // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
            snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { contentPadding ->
        // ...
    }
}

Yukarıdaki kodda, durum bir hata içeriyorsa eş yordam tetiklenir, hata yoksa işlem iptal edilir. LaunchedEffect çağrısı sitesi if ifadesi içinde yer aldığından, ifade yanlış olduğunda LaunchedEffect Bestede yer alıyorsa bu ifade kaldırılır ve eş yordam iptal edilir.

rememberCoroutineScope: composable dışında bir eş yordam başlatmak için besteye duyarlı bir kapsam elde etme

LaunchedEffect, composable bir işlev olduğundan yalnızca diğer composable işlevlerin içinde kullanılabilir. composable'ın dışında bir eş yordam başlatmak, ancak besteden ayrıldığında otomatik olarak iptal edilecek şekilde kapsamını ayarlamak için rememberCoroutineScope değerini kullanın. Bir veya daha fazla eşin yaşam döngüsünü manuel olarak kontrol etmeniz gerektiğinde (örneğin, bir kullanıcı etkinliği gerçekleştiğinde bir animasyonu iptal etmek) rememberCoroutineScope de kullanabilirsiniz.

rememberCoroutineScope, Beste'nin çağrıldığı noktaya bir CoroutineScope bağlı değeri döndüren composable bir işlevdir. Çağrı, Besteden ayrıldığında kapsam iptal edilir.

Önceki örneğe göre, kullanıcı Button öğesine dokunduğunda bir 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 başvuruda bulunun

LaunchedEffect, temel parametrelerden biri değiştiğinde yeniden başlatılır. Ancak bazı durumlarda, etkisinize bir değer kaydetmek isteyebilirsiniz. Bu değer, değişirse etkinin yeniden başlamasını istemezsiniz. Bunu yapmak için rememberUpdatedState kullanarak bu değere yakalanıp güncellenebilecek bir referans oluşturmanız gerekir. Bu yaklaşım, yeniden başlatılması pahalı veya yasaklayıcı olabilecek uzun süreli işlemler içeren etkiler için faydalıdır.

Örneğin, uygulamanızda bir süre sonra kaybolan bir LandingScreen bulunduğunu varsayalım. LandingScreen yeniden derlenmiş olsa bile biraz bekleyip geçen sürenin yeniden başlatılmaması gerektiğini bildiren efekt:

@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 parametre olarak Unit veya true gibi asla değişmeyen bir sabit değer iletilir. Yukarıdaki kodda LaunchedEffect(true) kullanılmıştır. onTimeout lambda'nın her zaman LandingScreen yeniden derlenen en son değeri içerdiğinden emin olmak için onTimeout işlevinin rememberUpdatedState işleviyle sarmalanması gerekir. Döndürülen State (koddaki currentOnTimeout), efektte kullanılmalıdır.

DisposableEffect: Temizlik gerektiren efektler

Tuşlar değiştikten sonra temizlenmesi gereken yan etkiler veya composable, Besteden ayrılırsa DisposableEffect değerini kullanın. DisposableEffect anahtarları değişirse composable'ın mevcut efektini silmesi (temizliği yapması) ve efekti tekrar çağırarak sıfırlanması gerekir.

Örneğin, LifecycleObserver kullanarak Lifecycle etkinliklerine dayalı analiz etkinlikleri göndermek isteyebilirsiniz. Compose'da bu etkinlikleri dinlemek için DisposableEffect kullanarak gözlemcinin kaydını gerektiğinde iptal edin.

@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, bu işlem observer öğesini lifecycleOwner öğesine ekler. lifecycleOwner değişirse etki kaldırılır ve yeni lifecycleOwner ile yeniden başlatılır.

DisposableEffect, kod bloğunda son ifade olarak onDispose ifadesini içermelidir. Aksi takdirde, IDE, derleme zamanı hatası görüntüler.

SideEffect: Oluşturma durumunu Oluşturma olmayan kodda yayınla

Oluşturma durumunu, 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 uygulanacağını garanti eder. Diğer yandan, doğrudan bir composable'a efekt yazarken başarılı bir yeniden kompozisyon elde edilmeden önce bir efekt gerçekleştirmek yanlıştır.

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

produceState, değerleri döndürülen bir State öğesine aktarabilen Beste kapsamındaki bir eş yordamı başlatır. Oluşturma dışı durumu E-posta Yaz durumuna dönüştürmek için (örneğin Flow, LiveData veya RxJava gibi abonelik odaklı harici durumları Beste'ye eklemek) bunu kullanın.

Yapımcı, produceState Besteye girdiğinde başlatılır ve Beste'den çıktığında iptal edilir. Döndürülen State birleşir. Aynı değerin ayarlanması yeniden oluşturma işlemini tetiklemez.

produceState eş yordam oluştursa da, harcama yapmayan 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 resim yüklemek için produceState özelliğinin 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ün

Compose'da gözlemlenen bir durum nesnesi veya composable giriş her değiştiğinde yeniden oluşturma işlemi gerçekleşir. Bir durum nesnesi veya girişi, kullanıcı arayüzünün gerçekten güncellenmesi gerekenden daha sık değişerek gereksiz yeniden yapılandırmaya yol açabilir.

Bir composable'a girişleriniz yeniden oluşturmanız gerekenden daha sık değişiyorsa derivedStateOf işlevini kullanmalısınız. Bu, genellikle kaydırma konumu gibi bir öğe sık sık değiştiğinde ancak composable'ın yalnızca belirli bir eşiği aştığında buna tepki vermesi gerektiğinde ortaya çıkar. derivedStateOf, gözlemleyebileceğiniz yeni bir Compose durumu nesnesi oluşturur. Bu nesne yalnızca ihtiyacınız kadar güncellenir. Bu sayede, Kotlin Flows distinctUntilChanged() operatörüne benzer şekilde çalışır.

Doğru kullanım

Aşağıdaki snippet, derivedStateOf için uygun bir kullanım alanını göstermektedir:

@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ğişir. Kaydırdığınızda değer 0, 1, 2, 3, 4, 5 vb. olur. Bununla birlikte, yeniden oluşturma işleminin yalnızca değer 0 değerinden büyükse yapılması gerekir. Güncelleme sıklığındaki bu uyumsuzluk, bunun derivedStateOf için iyi bir kullanım alanı olduğu anlamına gelir.

Yanlış kullanım

İki Compose durum nesnesini birleştirdiğinizde "durumu türettiğiniz" için derivedStateOf kullanmanız gerektiğini varsaymak sık yapılan bir hatadır. Ancak bu, aşağıdaki snippet'te gösterildiği gibi tamamen genel bir işlemdir 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 de firstName ve lastName kadar sık güncellenmelidir. Bu nedenle, aşırı bir yeniden oluşturma işlemi gerçekleşmez ve derivedStateOf kullanılması gerekmez.

snapshotFlow: Oluştur'un Durumunu Akışlara dönüştürün

State<T> nesnelerini bir soğuk Akışa dönüştürmek için snapshotFlow aracını kullanın. snapshotFlow, toplandığında bloğunu çalıştırır ve içinde okunan State nesnelerinin sonucunu çıkarır. snapshotFlow bloğunun içinde okunan State nesnelerinden biri değiştiğinde, yeni değer önceki yayınlanan değerle eşit değilse Akış, yeni değeri toplayıcıya yayınlar (bu davranış Flow.distinctUntilChanged karakterine benzer).

Aşağıdaki örnekte, kullanıcı Analytics'e erişmek için bir listedeki ilk öğeyi geçtikten sonra bunu 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, Akış operatörlerinin gücünden yararlanan bir Akışa dönüştürülür.

Yeniden başlatma efektleri

Oluşturma'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 veya anahtar kullanır.

Bu API'lerin tipik biçimi:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

Bu davranışın inceliklerinden dolayı, efekti yeniden başlatmak için kullanılan parametreler doğru değilse sorunlar oluşabilir:

  • Yeniden başlatmanın olması gerekenden daha az etkileri, uygulamanızda hatalara neden olabilir.
  • Yeniden başlatmanın olması gerekenden fazla etkileri olması verimsiz olabilir.

Genel kural olarak, efekt blokunda kullanılan değişken ve sabit değişkenler, composable efektine parametre olarak eklenmelidir. Efektin yeniden başlatılmasını sağlamak için bunların dışında daha fazla parametre eklenebilir. Bir değişkendeki değişiklik, etkinin yeniden başlatılmasına neden olmayacaksa değişken rememberUpdatedState içine yerleştirilmelidir. Değişken, anahtarı olmadan bir remember içine sarmalandığı için hiçbir zaman değişmezse değişkeni efekt için anahtar olarak aktarmanız gerekmez.

Yukarıda gösterilen DisposableEffect kodunda, bu etki, blokunda kullanılan lifecycleOwner parametresi olarak kabul edilir, çünkü bunlarda yapılan herhangi bir değişiklik etkinin yeniden başlatılması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)
        }
    }
}

rememberUpdatedState kullanımı nedeniyle Bileşim'de değerleri hiçbir zaman değişmediğinden currentOnStart ve currentOnStop, DisposableEffect anahtarları olarak gerekli değildir. lifecycleOwner parametresini parametre olarak iletmezseniz ve değiştirirse HomeScreen yeniden oluşturur ancak DisposableEffect atılmaz ve yeniden başlatılır. Bu durum, o noktadan itibaren yanlış lifecycleOwner kullanıldığından sorunlara yol açar.

Tuş olarak sabitler

true gibi bir sabit değeri kullanarak çağrı sitesinin yaşam döngüsünü takip etmesini sağlayabilirsiniz. Yukarıda gösterilen LaunchedEffect örneğindeki gibi bunun geçerli kullanım alanları vardır. Ancak bunu yapmadan önce iki kez düşünün ve ihtiyacınız olan şeyin bu olduğundan emin olun.