Part 1 — Compose Gibi Düşünmek
Merhabalar bu yazı serisinde https://developer.android.com/develop/ui/compose/documentation sitesindeki dokümantasyona göre adım adım Compose yazılarını kendimce anlatmaya çalışacağım. İlk Adım olarak Thinking in Compose kısmından başlıyoruz.
Jetpack compose bildirimsel bir UI araç setidir. Declarative(bildirimsel) Api sağlayarak kullanıcı arayüzünü çizdirmemizi sağlar.
Declarative Programlama’dan kasıt nedir?
Biliyorsunuz ki XML’de bir Component tree katmanımız vardı ve burada bileşenlerimizi ekliyorduk. Ardından Activity veya Fragment’lardan findViewById veya binding işlemleri ile bileşenlere erişip state durumlarına göre bunları güncelliyorduk. Örneğin, img.setImageBitmap(Bitmap)
gibi… Bu bileşenleri manuel değiştirmek, projemiz karmaşıklaştığında bize çok sıkıntı çıkarıyordu. Bileşenler uyumsuz bir şekilde değiştirilebiliyor veya bir yerde değişip diğer yerde değiştirilmesi unutuluyordu.
Fakat Declarative UI modelinde, belirli bir işlemin nasıl yapılacağını belirtmek yerine, ne yapılması gerektiğini ifade ederiz. Uygulamamız ilk çalıştığında tüm ekran sıfırdan çizilir ve sonra sadece gerekli bileşenler, yani state’i veya verisi değişen bileşenler, tekrardan çizilir. Bu şekilde bir ekran çizilmesine Recomposition diyoruz.
Bir Composable Fonksiyon Nasıl Olmalı?
@Composable
fun Greeting (name: String){
Text ("Hello $name")
}
1- Fonksiyon @Composable anotasyonu ile işaretlenmeli.
2- Composable fonksiyonlar parametre alabilir.
3- Composable fonksiyonlar kendi içerisinde başka bir composable fonksiyon çağırabilir. Örnekte Text() fonksiyonu da composable bir fonksiyondur zaten.
4- Composable fonksiyonların bir şey return etmesine gerek yoktur çünkü ekrana bileşen çizmek temel amacıdır.
Declarative Değişimler?
Imperative UI bileşenlerinde mesela Xml’de , her bileşenin kendi state’i vardır. Ve uygulamada bu state’lerle iletişime girmek için getter ve setter metotlarını kullanırız.
Declarative yaklaşımda ise getter ve setter metodları açığa çıkmaz. Bileşenler nesne gibi davranmaz, fonksiyon gibi davranırlar. Composable olan bu fonksiyonları farklı parametrelerle çağırarak yeni verilerle tekrardan çizilmesini sağlarız. Onclick gibi işlemlerle kullanıcı , arayüzle etkileşime girdiğinde uygulamaya bildirilir ve state veya veri değiştiğinde , composable fonksiyon tekrardan ekranda güncel verilerle çizilir. Buna Recomposition denir.
Recomposition
Imperative UI düzeninde setter metotlarıyla bileşeninin state’ini değiştiririz. Compose’da ise Composable fonksiyonu yeni datayla çağırırız ve Compose Frawework’u sadece değişen bileşeni yeni veriyle tekrardan çizer.
Recomposition, yalnızca state veya veri değiştiğinde değil, Compose’un gereksinim duyduğu herhangi bir durumda gerçekleşebilir. Bu, yalnızca state değişiklikleriyle sınırlı değildir. Ayrıca, Compose’un mevcut durumu optimize etmek için tekrar tekrar çağırması da mümkündür.
Aşağıdaki kodu inceleyelim:
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("bana $clicks kere tıkladın!")
}
}
Butona her tıklandığında clicks değeri değişiyorsa Compose, Text() fonksiyonunu yeniden çizdirir. Eğer tüm kullanıcı arayüzü her defasında değişseydi çok büyük performans isterdi ve bu performans cihazımızın batarya ömrünü azaltır , uygulamamızda farklı donma ve kasma sorunları ortaya çıkarabilirdi.
Bir fonksiyonun yeniden çizilmesi atlanabilir, yarıda kalabilir veya iptal olabilir. Bu yüzden bir composable fonksiyondaki side-effects olarak yazdığınız kodlara asla güvenmeyin mümkünse yazmayın, önerilmez.
Side-effects dediğimiz kodlar aşağıdaki gibi olabilir:
1-Global bir nesnenin verisinin değiştirilmesi
2- Viewmodel’de bir observable property’i güncelleme
3- Shared Preferences verisini güncelleme
vs gibi işlemlere side-effects denir.
Recomposition işlemi mümkün olan en hızlı şekilde gerçekleşmesi gereklidir. Bu yüzden ağır işlemleri composable fonksiyonlarda yapmayınız. Viewmodelde veya herhangi bir class’ta Coroutine kullanarak arka planda yapıp sonucunu composable fonksiyona vermelisiniz.
@Composable
fun SharedPrefsToggle(
text: String,
value: Boolean,
onValueChanged: (Boolean) -> Unit
) {
Row {
Text(text)
Checkbox(checked = value, onCheckedChange = onValueChanged)
}
}
Örneğin yukarıdaki kod’a shared’den aldığımız verileri veya butona tıklanınca shared’ten veriyi almayı yaptırtabiliriz. Sadece sonuçları döndürüyoruz. Temiz olan bu şekildedir.
Composable Kullanırken Dikkat Edin
1- Composable fonksiyonlar sırayla değil, karışık şekilde çağrılır.
2- Composable fonksiyonlar paralel çağrılabilir.
3- Recomposition mümkün olduğunca composable fonksiyonunuzu veya lambdanızı atlamak ister.
4- Recomposition iptal olabilir, iyimser davranır.
5- Composable fonksiyon bir animasyonun her karesi gibi sıklıkla çalıştırılabilir.
Composable Fonksiyon Sırasız Çağrılabilir
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
Compose, bazı fonksiyonları bazılarından önce ekrana çizebilir. Yani yukarıdaki örnekteki StartScreen(), MiddleScreen(), EndScreen() fonksiyonları herhangi bir sırada çağrılabilir. Bu da demek oluyor ki StartScreen’deki bir veriye göre MiddleScreen() çizilimi uygulamanızda saçma ve absürt durumlar ortaya çıkarabilir. Bu nedenle her bir Composable fonksiyon kendi kendine yetmelidir başka bir fonksiyona mümkün olduğunca bağımlı olmamalıdır.
Composable Fonksiyon Paralel Çalıştırılabilir
Compose, birden fazla fonksiyonu paralel çağırarak birden fazla çekirdek kullanabilir. Yani arka planda bir thread havuzu vardır. Yani bir fonksiyon birkaç farklı yerden çağrılabilir. Bu yüzden side-effects kullanımları fonksiyonlar içinde önerilmez.
@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
var items = 0
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
items++ // side-effect kod
}
}
Text("Count: $items")
}
}
Önreğin yukarıdaki kodda items property’si her recomposition işleminde yeniden değiştirilir. Bu fazlaca değişim kullanıcı arayüzünde yanlış sayıyı gösterecektir. Bu yüzden bu şekil yazmalardan kaçınmak gerekir.
Recomposition Mümkün Olduğunca Geçiştirilir
Compose , arayüzde yalnızca güncellenmesi gerekilen yeri tekrar çizer. Her composable fonksiyon ve lambda kendi başına tekrar oluşabilir. Yani kendi verisi değiştiğinde güncellenir. Aşağıdaki kodda bunu inceleyebilirsiniz.
/**
* Kullanıcıya bir başlık yazısı ve bir liste gösteren kod
*/
@Composable
fun NamePicker(
header: String,
names: List<String>,
onNameClicked: (String) -> Unit
) {
Column {
// Bu Text fonksiyonu yalnızca header parametresi değiştiğinde tekrar çizilir
Text(header, style = MaterialTheme.typography.bodyLarge)
Divider()
// LazyColumn, Recycler View'in benzeri diyebiliriz
LazyColumn {
items(names) { name ->
// itemlerin name'leri değiştiğinde burası tekrar çizilir.
// header değiştiğinde burası tekrar çizilmez
NamePickerItem(name, onNameClicked)
}
}
}
}
/**
* kullanıcı tıkladığında ismi arayüzde gösteren kod
*/
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}
Recomposition İyimserdir
Recomposition yapılırken işlem daha bitirilmeden bir parametre değişirse, Compose, bu recomposition’u iptal edebilir ve yeni parametreyle yeniden başlatır. Recomposition iptal olursa compose bu UI ağacını atar ama eğer bu fonksiyonda bir side-effect olan kod satırı varsa , bu kod çalışacaktır ve bu da uygulamanızda tutarsızlıklara yol açabilir.
Sonuç
Bu partta Jetpack Compose’u ve bildirimsel UI yaklaşımını keşfettik. Declarative Programlama’nın, özellikle karmaşık projelerde, UI bileşenlerinin yönetimini nasıl kolaylaştırdığını gördük. Composable fonksiyonların nasıl çalıştığını, yeniden çizim (recomposition) süreçlerini ve Side-effect’lerin nasıl yönetilmesi gerektiğini öğrendik. Compose’un performans odaklı tasarımı ve UI bileşenlerini en verimli şekilde güncelleme stratejileri, modern Android uygulamaları geliştirme sürecini büyük ölçüde iyileştiriyor. Bu temel bilgilerle, Jetpack Compose ile daha etkin ve sürdürülebilir uygulamalar geliştirebilirsiniz.
Kaynaklar
https://developer.android.com/develop/ui/compose/mental-model