Jetpack Compose’a yeni başlayanların en çok zorlandığı konulardan biri durum (state) yönetimi. remember, mutableStateOf ve recomposition kavramları ilk bakışta karmaşık görünse de, doğru zihinsel modelle oldukça sade hâle geliyor: UI = f(state). Bu yazıda bu modeli adım adım kuruyoruz.

Compose’da “state” ne demek?

Compose’da bir arayüz, state’in bir fonksiyonudur: state değiştiğinde, o state’e bağlı UI otomatik olarak yeniden çizilir (recomposition). Bunun çalışabilmesi için Compose’un state değişikliğini “görmesi” gerekir; bu da bizi mutableStateOf‘a getirir:

var sayac by remember { mutableStateOf(0) }

Button(onClick = { sayac++ }) {
    Text("Tıklama sayısı: $sayac")
}

Burada mutableStateOf(0), Compose’un izleyebildiği (observable) bir state nesnesi oluşturur. sayac değiştiğinde, yalnızca bu değeri okuyan Text bileşeni yeniden çizilir, tüm ekran değil.

remember ne işe yarar?

remember, bir composable yeniden çalıştırıldığında (recomposition) değerin sıfırlanmasını engeller. remember olmadan, her recomposition’da state yeniden oluşturulur ve kullanıcı etkileşimi kaybolur:

// YANLIŞ: her recomposition'da sıfırlanır
var sayac = mutableStateOf(0)

// DOĞRU: recomposition'lar arasında korunur
var sayac by remember { mutableStateOf(0) }

remember‘ın önemli bir sınırı var: composable’ın yapılandırma değişikliği (configuration change, örneğin ekran döndürme) sonucunda yeniden oluşturulması (yani Activity’nin yeniden başlatılması) durumunda state kaybolur. Bu durumu önlemek için rememberSaveable kullanılır:

var kullaniciAdi by rememberSaveable { mutableStateOf("") }

rememberSaveable, değeri Android’in Bundle mekanizması üzerinden saklar; bu sayede ekran döndürme gibi olaylarda dahi form girdisi gibi kullanıcı verisi kaybolmaz.

Recomposition’ı ne tetikler?

Recomposition, okunan bir State nesnesinin değeri değiştiğinde tetiklenir. Compose, hangi composable’ların hangi state’i “okuduğunu” (snapshot sistemi aracılığıyla) izler ve yalnızca etkilenen kısımları yeniden çizer. Bu da Compose’u performans açısından verimli kılan temel mekanizmadır. Pratik sonuç: bir composable’ın çok geniş bir state nesnesini (örneğin koca bir veri sınıfını) okuması, o nesnenin herhangi bir alanı değiştiğinde gereksiz yere yeniden çizilmesine yol açabilir. Mümkün olduğunca ilgili composable’ın yalnızca ihtiyaç duyduğu alt state’i okumasını sağlamak, performansı doğrudan iyileştirir.

Gereksiz recomposition’ı azaltmak: derivedStateOf

Bir state, başka bir state’ten türetilen (hesaplanan) bir değerse ve bu hesaplama her recomposition’da tekrar tekrar yapılması gereksizse, derivedStateOf kullanılır:

val listState = rememberLazyListState()
val ilkOgeGorunuyorMu by remember {
    derivedStateOf { listState.firstVisibleItemIndex == 0 }
}

derivedStateOf, yalnızca bağımlılıkları (burada firstVisibleItemIndex) gerçekten değiştiğinde yeniden hesaplanır: kaydırma sırasında her piksel değişiminde değil, yalnızca sonuç (ilk öğenin görünüp görünmediği) değiştiğinde recomposition tetikler. Bu, özellikle liste kaydırma gibi yüksek frekanslı state güncellemelerinde belirgin bir performans farkı yaratır.

Yan etkiler: LaunchedEffect

State değişikliğine bağlı olarak bir “yan etki” (side effect), örneğin bir API çağrısı veya bir animasyon başlatma, çalıştırmanız gerektiğinde, doğrudan composable gövdesine bu kodu yazmak yanlıştır; bunun yerine LaunchedEffect kullanılır:

LaunchedEffect(kullaniciId) {
    viewModel.kullaniciyiYukle(kullaniciId)
}

LaunchedEffect‘in anahtarı (burada kullaniciId) değiştiğinde, önceki coroutine iptal edilir ve blok yeniden çalıştırılır. Bu da yalnızca ilgili değer değiştiğinde işlemin tekrarlanmasını garanti eder.

State’i yukarı taşımak (state hoisting)

İyi bir Compose mimarisinde state, genellikle onu kullanan composable’dan bir üst seviyeye taşınır (state hoisting). Bu, composable’ı “stateless” ve test edilebilir hâle getirir; state’in tek bir kaynaktan yönetilmesini sağlar:

@Composable
fun Sayac(sayi: Int, onArtir: () -> Unit) {
    Button(onClick = onArtir) { Text("$sayi") }
}

@Composable
fun SayacEkrani() {
    var sayi by remember { mutableStateOf(0) }
    Sayac(sayi = sayi, onArtir = { sayi++ })
}

Sayac composable’ı artık kendi state’ini yönetmiyor; state’i ve state’i değiştiren fonksiyonu parametre olarak alıyor. Bu desen, aynı composable’ın farklı state kaynaklarıyla (örneğin bir ViewModel’den gelen state ile) yeniden kullanılabilmesini sağlar.

ViewModel ve StateFlow ile birlikte kullanım

Ekran seviyesinde state genellikle bir ViewModel içinde tutulur ve StateFlow üzerinden Compose’a aktarılır. Bu, konfigürasyon değişikliklerinde state’in kaybolmamasını sağlar, çünkü ViewModel, Activity yeniden oluşturulsa bile hayatta kalır:

class SayacViewModel : ViewModel() {
    private val _sayi = MutableStateFlow(0)
    val sayi: StateFlow<Int> = _sayi.asStateFlow()

    fun artir() {
        _sayi.value += 1
    }
}

@Composable
fun SayacEkrani(viewModel: SayacViewModel = viewModel()) {
    val sayi by viewModel.sayi.collectAsStateWithLifecycle()
    Sayac(sayi = sayi, onArtir = viewModel::artir)
}

collectAsStateWithLifecycle, StateFlow‘u yalnızca ekran görünür durumdayken toplar (collect). Bu, arka planda gereksiz güncelleme işlemesini önleyerek pil ve kaynak tüketimini azaltır. Basit, ekrana özel geçici state için remember yeterliyken; birden fazla composable arasında paylaşılan veya asenkron veri kaynağına bağlı state için ViewModel + StateFlow kombinasyonu daha sağlam bir temel sağlar.

Sonuç

Compose’da durum yönetimini anlamanın anahtarı, “UI = f(state)” prensibini benimsemek. Bu zihinsel modelle, remember/rememberSaveable ile kalıcılık, derivedStateOf ile gereksiz hesaplamayı önleme, state hoisting ile test edilebilirlik ve ViewModel + StateFlow ile ekran ömrünü aşan durum yönetimi birbirini tamamlayan parçalar hâline geliyor. Bir sonraki yazıda, bu prensipleri Kotlin Multiplatform projelerinde paylaşılan bir ViewModel katmanıyla nasıl birleştirdiğimizi ele alacağız.