Jetpack Compose Side Effect’ler

Ömer Durmaz
4 min readOct 15, 2023

Merhaba bu yazımızda herhangi bir Jetpack Compose uygulama geliştirmeden önce bilmemiz gereken Side effectlerden bahsedeceğiz.

Nedir bu side effectler?

Bildiğiniz üzere xml geliştirilen android uygulamalarda ekranların yaşam döngülerine göre bazı işlemler yapabiliyoruz. Bunlara onCreate ya da onResume methodlarını örnek verebiliriz. Jetpack compose ile geliştirilen uygulamalarda ise her bir composable methodu bir ekran gibi düşünebiliriz. Ancak direkt composable method içerisinde method create mi oluyor yoksa destroy mu oluyor dinleyemeyiz. Bunun en büyük sebebi composable methodlar bazı durumlarda peş peşe birçok kez çalışabiliyor. Eğer onCreate için gerekli kodları direkt composable method içine yazarsak bu o kodun peş peşe tekrar çalşmasına sebep olacaktır. Bu side effectlerin faydalı olduğu alanlardan birisi. Ayrıca yaşam döngüsü değişikliklerinin yanında bir parametrenin değiştiği durumlarda da methodun çalışmasını istiyorsak yine side effectlerin kullanılması gerekmektedir. Hadi şu side effectlere daha yakından bakalım:

LaunchedEffect

LaunchedEffect içine verilen parametre her güncellendiğinde tetiklenen bir methoddur. Eğer içine verdiğiniz parametre hiç güncellenmeyecekse bile Composable method ilk kez çalıştığında bir kere çalışacaktır.

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

Yukarıda LaunchedEffect methodunun aldığı parametreleri görüyorsunuz. key parametresi sadece bir tane görünse de sınırsızdır. Birden fazla parametreye aynı anda bakabilir. block parametresi ise görüldüğü üzere coroutine scope içinde bir callback methodtur. Bu da demektir ki Side effectler içerisindeki işlemler asenkron olarak çalışırlar.

Dipnot: Yukarıda da dediğimiz gibi side effectler coroutine method içerisinde çalıştığı için ilk işlem bitmeden izlenen parametre değişirse bir önceki scope işlemi iptal edilir ve yeni işlem başlatılır.

DisposableEffect

Bu side effect diğerleri LaunchedEffect gibi bir parametrenin değişmesi durumunda ya da composable methodun yaşam döngüsü değişmesi durumlarında çalışır. LaunchedEffect den farklı olarak kendi içerisinde onDispose callback methodu bulunur. Bu method composable methodumuz ekrandan kaldırıldığı anda tetiklenir. Bu sayede eğer ekran mevcut ekran kapandığında yapmak istediğimiz bir işlem var ise (Örn: veri tabanı bağlantısını kapatmak) bunun içinde yapabiliriz.

val dbConnection = remember { createDatabaseConnection() }

DisposableEffect(Unit) {
dbConnection.connect()

onDispose {
dbConnection.close()
}
}

Yukarıdaki DisposableEffect kodunu inceleyecek olursak; key parametresi olarak Unit değeri verilmiş, bu da demek oluyor ki parametre hiçbir zaman değişmeyecek. İçerisinde bulunan kodumuz da sadece composable methodumuz ilk kez oluştuğunda çalışacak ve composable methodumuz ekrandan kaldırılırken de onDispose içindeki kodumuz çalışacak.

Bu sayede veri kaynadığının daha düzgün yönetilmesini sağlayabiliriz. Örneğin açık olan bir veritabanı bağlantısını kapatmak ya da hali hazırda kullanılan bir telefon özeliğinin kapatılması gibi işleri bu alan içerisinde yapabiliriz.

SideEffect

Bu side effectimiz ise composable methodumuz içerisindeki bir değer her güncellendiğinde tekrar çalışır. Bu sayede herhangi bir yeni değer durumuna göre yapmak istediğimiz bir işlem var ise bunu yapabiliriz.

@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }

// Doğrudan Composable içine kod yazma
count++

Text("Count: $count")
}

Bu örnekte, Counter adlı Composable işlevi, her çağırıldığında count değerini artırır ve günceller. Bu, her çağırıldığında count değerinin artırılmasını sağlar.

Örnek: SideEffect İşlevi Kullanmak

@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }

SideEffect {
// count değeri her değiştiğinde çalışacak kodlar
count++
println("Count değeri güncellendi: $count")
}

Text("Count: $count")
}

Bu örnekte, SideEffect işlevi kullanıyoruz ve count değeri her değiştiğinde çalışacak kodları içeriyor. Yani her count değeri değiştiğinde, bu kodlar çalışır ve count değeri güncellenir. SideEffect içindeki kodlar, her count değişikliğinde çalıştığı için her güncelleme sonrası count değeri 1 artırılır.

Farkı anlamak için her iki örneği karşılaştırabiliriz:

  • İlk örnekte, count değeri her çağırıldığında artırılır ve güncellenir, ancak bu sadece Counter işlevi içinde çalışır.
  • İkinci örnekte ise, SideEffect içindeki kodlar, her count değişikliğinde çalışır ve bu nedenle her güncelleme sonrası count değeri artırılır.

Yani, SideEffect işlevini kullanarak count değeri her güncellendiğinde çalışacak kodları belirlemiş oluyoruz. Eğer bu kodları doğrudan Composable içine yazsaydık, her çağırıldığında çalışır ve count değeri her seferinde artmazdı. Bu nedenle SideEffect, belirli bir veri değişikliği veya koşula bağlı olarak işlem yapmak istediğinizde kullanışlıdır.

rememberCoroutineScope

Composable methodlar içerisinde nasıl asenkron ya da methodun güncellenmesine göre işlemler yapabileceğimizden bahsettik. Peki ya bir alt composable methoddan gelen gelen eventlere göre işlem yapmamız gerekirse? Ya da daha açık şekilde bir button composablenin click eventi bulunmakta ve kullanıcı butona tıkladığında api isteği atmamız gerekirse ne yapmalıyız?

Biliyoruz ki api isteği gibi işlerin hepsini asenkron şekilde coroutine scopelar içerisinde yapmamız gerekir. Bunun ana sebebi genel uygulama akışını kilitlememektir. LaunchedEffect ya da DisposableEffect gibi side effectleri dolaylı yoldan tetikleyerek coroutine scope içerisinde istek atmamız mümkün. Ancak jetpack compose ayrıca rememberCoroutineScope methodu sayesinde composable methodlar içerisinde ayrı bir coroutine scope oluşturmamıza olanak tanıyor.

@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {

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")
}
}
}
}

Yukarıdaki örnekte görüldüğü gibi butona tıklandığında snackbar gösterilme işi coroutineScope içerisinde yapılıyor. Bunun sebebi snackbarın barındırdığı animasyonlardır. Asenkron çalıştırarak ana threadi kitlemeden animasyonu çalıştırmış oluyoruz.

rememberUpdatedState

Bu side effectimiz ise kendisine verilen değişkenin güncellenme durumunu kontrol eder. Eğer verilen değişkenin güncellenmez ise kendisini güncellemez ancak verilen değişken güncellenirse kendisi de bu güncel değeri tutar ve kendisini dinleyen viewlerin de güncellenmesini sağlar.

@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
val updatedCount by rememberUpdatedState(count)

Button(onClick = { count++ }) {
Text("Artır")
}

Text("Count: $updatedCount")

val isEven = updatedCount % 2 == 0
val message = if (isEven) "Çift sayıdır." else "Tek sayıdır."
Text(message)
}

yukarıdaki örnekte göreceğimiz üzere count değeri çift ya da tek olarak güncellendiğinde updatedCount değerine bakıyor ve texti çiziyoruz. Ancak eğer count değerine her seferinde aynı değeri atıyor olsaydık ekran sadece bir kez güncelleniyor olacaktı.

Bu yazımızda da side effectlerin projemizdeki kullanımlarından ve ne işe yaradıklarından bahsettik. Umarım faydalı olmuştur ve aklınızdaki soruları gidermeye yaramıştır. Bir sonraki yazıda görüşmek üzere Ciao!

--

--