Oluşturma'daki yan etkiler

Yan etki, composable işlevin kapsamı dışında gerçekleşen uygulama durumunda yapılan bir değişikliktir. composable'ların yaşam döngüsü ve öngörülemeyen yeniden derlemeler, composable'ların farklı sıralarda yeniden kompozisyonlarının yürütmesi ya da atılabilecek yeniden kompozisyonlar gibi özellikleri nedeniyle, tercihen yan etki içermeyen olmalıdır.

Bununla birlikte, bazen yan etkiler, örneğin bir atıştırmalık çubuğunu göstermek veya belirli bir durum koşulunda başka bir ekrana gitmek gibi tek seferlik bir etkinliği tetiklemek için gereklidir. Bu işlemler, composable'ın yaşam döngüsünden haberdar 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 sonuç kullanım alanları

Thinking in Compose belgelerinde açıklandığı gibi composable'lar yan etki içermemelidir. Uygulamanın durumunda değişiklik yapmanız gerektiğinde (Yönetim belgeleri belgesinde açıklandığı gibi), bu yan etkilerin öngörülebilir bir şekilde yürütülmesi için Effect API'lerini kullanmanız gerekir.

Compose'da ortaya çıkan farklı olasılık efektleri nedeniyle bunlar kolayca fazla kullanılabilir. Bu öğelerde yaptığınız çalışmanın kullanıcı arayüzü ile ilgili olduğundan ve Yönetim durumu 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 kullanım ömrü boyunca iş yapmak ve askıya alma işlevlerini çağırmak için LaunchedEffect composable'ı kullanın. LaunchedEffect Beste'ye girdiğinde, parametre olarak iletilen kod bloğunu içeren 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, alfa değerini yapılandırılabilir bir gecikmeyle yanıp sönen bir animasyon:

// 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 ayarlanan süreyi beklemek için askıya alma işlevini delay kullanır. Daha sonra, alfa animasyonu sırayla sıfıra ayarlanır ve animateTo kullanılarak geri döndürülür. Bu işlem, composable'ın kullanım ömrü boyunca tekrarlanır.

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

LaunchedEffect, composable bir işlev olduğundan yalnızca diğer composable işlevlerin içinde kullanılabilir. Bir composable'ın dışında eş yordam başlatmak için, ancak kapsamı, besteden ayrıldıktan sonra otomatik olarak iptal edilecek şekilde ayarlamak 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 (örneğin, bir kullanıcı etkinliği gerçekleştiğinde bir animasyonu iptal etmek) rememberCoroutineScope kullanın.

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

Önceki örneğe göre, kullanıcı bir Button öğesine dokunduğunda Snackbar görüntülemek 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: Efektte, değer değişirse yeniden başlatılmaması gereken bir değere başvuru

Anahtar parametrelerden biri değiştiğinde LaunchedEffect yeniden başlatılır. Ancak bazı durumlarda, değişirse efektin yeniden başlatılmasını istemeyeceğiniz bir değeri yakalamak isteyebilirsiniz. Bunu yapmak için rememberUpdatedState kullanarak bu değere kaydedilip güncellenebilecek bir referans oluşturmalısınız. Bu yaklaşım, yeniden oluşturulması ve yeniden başlatılması pahalı veya engelleyici olabilecek uzun ömürlü işlemler içeren efektler için faydalıdır.

Örneğin, uygulamanızda bir süre sonra kaybolan LandingScreen olduğunu varsayalım. LandingScreen yeniden oluşturulsa bile, bir süre bekleyen ve 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 */
}

Telefon araması sitesinin yaşam döngüsüyle eşleşen bir efekt oluşturmak için Unit veya true gibi asla değişmeyen bir sabit değer parametre olarak aktarılır. Yukarıdaki kodda LaunchedEffect(true) kullanılmıştır. onTimeout lambdasının her zaman LandingScreen ile 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) işlevde kullanılmalıdır.

DisposableEffect: Temizlik gerektiren efektler

Tuşlar değiştirildikten sonra temizlenmesi gereken veya composable'ın besteden ayrılması gereken yan efektler için DisposableEffect simgesini kullanın. DisposableEffect tuşları değişirse composable'ın mevcut etkisini ortadan çıkarması (temizleme 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özlemciyi kaydedin ve gerektiğinde gözlemcinin kaydını 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 efekt, observer öğesini lifecycleOwner öğesine ekler. lifecycleOwner değişirse efekt 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'de derleme zamanı hatası gösterilir.

SideEffect: Oluştur durumunu, Compose olmayan koda yayınla

Compose durumunu, oluşturma tarafından yönetilmeyen nesnelerle paylaşmak için SideEffect composable'ı kullanın. SideEffect kullanılması, etkinin her başarılı yeniden düzenlemeden sonra gerçekleşmesini garanti eder. Diğer yandan, başarılı bir yeniden oluşturma garanti edilmeden önce bir efektin gerçekleştirilmesi yanlıştır. Bu, efektin doğrudan composable'a yazılması durumunda geçerlidir.

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

produceState, değerleri döndürülen State öğesine aktarabilen Besteye ayarlanan bir eş yordam başlatır. Oluşturma dışı durumu Compose durumuna dönüştürmek için bunu kullanın. Örneğin Flow, LiveData veya RxJava gibi harici abonelik odaklı durumları Beste'ye ekleyin.

Yapımcı, produceState Beste'ye girdiğinde başlatılır ve Beste'den ayrıldığı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 alma amacı taşımayan veri kaynaklarını gözlemlemek için de kullanılabilir. Bu 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 ö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ür

Compose'da gözlemlenen bir durum nesnesi veya composable girdi her değiştiğinde yeniden düzenleme gerçekleşir. Bir durum nesnesi veya giriş, kullanıcı arayüzünün gerçekte güncellenmesi gerekenden daha sık değişiyor olabilir. Bu da gereksiz yeniden yapılandırmaya yol açar.

Bir composable'a yaptığınız girişler yeniden oluşturmanız gerekenden daha sık değişiyorsa derivedStateOf işlevini kullanmalısınız. Bu durum genellikle kaydırma konumu gibi bir öğe sık sık değiştiğinde ortaya çıkar. Ancak composable yalnızca belirli bir eşiği aştığında buna tepki vermesi gerekir. derivedStateOf, yalnızca ihtiyacınız olduğu kadar güncellendiğini gözlemleyebileceğiniz yeni bir Oluştur durum nesnesi oluşturur. Bu şekilde, Kotlin Akışları 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, ilk görünür öğe her değiştiğinde firstVisibleItemIndex değişir. Kaydırırken değer 0, 1, 2, 3, 4, 5 vb. olur. Ancak, yeniden düzenleme yalnızca değer 0 değerinden büyükse gerçekleştirilmelidir. Güncelleme sıklığındaki bu uyumsuzluk, bunun derivedStateOf için iyi bir kullanım alanı olduğu anlamına gelir.

Hatalı kullanım

İki Compose durum nesnesini birleştirdiğinizde "türetme durumu" olduğunuzdan derivedStateOf kullanmanız gerektiğini varsaymak yaygın yapılan bir hatadır. Bununla birlikte, bu işlem tamamen ek yük oluşturur ve aşağıdaki snippet'te gösterildiği gibi 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 öğesinin, firstName ve lastName kadar sık güncellenmesi gerekir. Bu nedenle, aşırı yeniden oluşturma işlemi yapılmaz ve derivedStateOf kullanılması gerekmez.

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

State<T> nesnelerini 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 nesnelerin sonucunu yayınlar. snapshotFlow bloğunun içinde okunan State nesneden 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 aktarır (bu davranış Flow.distinctUntilChanged değerine benzerdir).

Aşağıdaki örnekte, kullanıcı sayfayı kaydırarak analiz listesine giden ilk öğeyi kaydıran bir yan efekt 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 yararlanabilecek bir Akışa dönüştürülür.

Yeniden başlatma efektleri

Compose'daki LaunchedEffect, produceState veya DisposableEffect gibi bazı efektler, çalışan efektini iptal etmek ve yeni anahtarlarla yeni bir tane başlatmak için kullanılan çeşitli sayıda bağımsız değişken ve anahtar alır.

Bu API'lerin tipik biçimi şu şekildedir:

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

Bu davranışın incelikleri nedeniyle, efekti yeniden başlatmak için kullanılan parametreler doğru parametreler değilse sorunlar ortaya çıkabilir:

  • Efektleri olması gerekenden daha az yeniden başlatmak uygulamanızda hatalara neden olabilir.
  • Efektleri olması gerekenden daha fazla yeniden başlatmak verimsiz olabilir.

Genel bir kural olarak, kodun efekt bloğunda kullanılan değişebilir ve değiştirilemez değişkenler, composable efekt için parametre olarak eklenmelidir. Bunların dışında, efektin yeniden başlatılmasını zorunlu kılmak için daha fazla parametre eklenebilir. Bir değişkendeki değişikliğin etkinin yeniden başlatılmasına neden olmaması gerekiyorsa değişken rememberUpdatedState içine alınmalıdır. Değişken, herhangi bir anahtar olmadan remember içine sarmalandığı için hiçbir zaman değişmiyorsa değişkeni, efekt için anahtar olarak iletmeniz gerekmez.

Yukarıda gösterilen DisposableEffect kodunda bu efekt, lifecycleOwner bloğunda kullanılan bir parametre olarak yansıtılır. Çünkü bu kurallarda 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)
        }
    }
}

currentOnStart ve currentOnStop değerleri rememberUpdatedState kullanımı nedeniyle Beste'de hiçbir zaman değişmediğinden DisposableEffect anahtarı olarak gerekli değildir. Parametre olarak lifecycleOwner parametresini iletmezseniz ve değişirse HomeScreen yeniden oluşturulur, ancak DisposableEffect imha edilmez ve yeniden başlatılır. Bundan sonra yanlış lifecycleOwner kullanıldığından bu durum sorunlara yol açar.

Anahtar olarak sabitler

Efekt anahtarı olarak true gibi bir sabit değer kullanarak anahtarın telefonla arama sitesinin yaşam döngüsünü takip etmesini sağlayabilirsiniz. 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 ihtiyacınız olanın bu olduğundan emin olun.