Android Jetpack Compose ile UiState Yönetimi

Mesutusta
inventiv
Published in
5 min readMay 25, 2023
https://developer.android.com/courses/jetpack-compose/course

Bu makalede Jetpack Compose ile ekran datasının nasıl yönetilmesi gerektiğinden bahsedeceğiz. Bu konu ile alakalı karşılaştığımız problemler ve çözümü inceleyeceğiz.

Konu ile alakalı teknik kısma girmeden önce kavramlardan bahsedelim.

UI State Nedir

Jetpack Compose, bildirimlerin ve durumların yönetimi için state adında bir yapı sunar. state, bir değişkenin değerini tutar ve bu değişkenin değeri değiştirildiğinde, state otomatik olarak yeniden çizilir. Bu sayede, uygulamanızdaki değişiklikler anında kullanıcı arayüzünde görüntülenebilir. Örneğin, bir formun doldurulma durumu, bir liste öğelerinin yüklenme durumu veya bir kullanıcının oturum açma durumu gibi durumlar UiState ile temsil edilebilir.

Jetpack Compose Önceki Yaklaşımı

Jetpack Compose öncesinde state yönetimi için Android’de genellikle ViewModel, LiveData ve Flow gibi yapılar kullanılır. ViewModel, UI ile ilgili verileri saklamak ve yönetmek için kullanılırken, LiveData ise veri değişikliklerini gözlemlemek ve UI’yi otomatik olarak güncellemek için kullanılır. Flow, veri kaynaklarından gelen verileri akış halinde UI bileşenlerine aktarır ve bu sayede UI bileşenleri, veri kaynaklarındaki değişiklikleri anında yansıtabilir. Bu da, UI bileşenlerinin daha hızlı ve daha doğru bir şekilde güncellenmesini sağlar.

Özetle, Jetpack Compose öncesinde Android uygulamalarında state yönetimi ve UI güncellemeleri için XML ve Java/Kotlin kodları kullanılırken, ViewModel, LiveData, Flow gibi yapılar ile data binding kullanılarak UI bileşenleri ve veri kaynakları arasında otomatik güncellemeler sağlanır. Jetpack Compose ile birlikte, bu süreç daha basit ve daha esnek hale gelmiştir.

Composition nedir

Composition, Jetpack Compose’un ana prensiplerinden biridir ve geliştiricilere esneklik, yeniden kullanılabilirlik ve kolaylık sağlar.

Jetpack Compose’da, UI bileşenleri Composable fonksiyonlar olarak tanımlanır. Bu fonksiyonlar, UI’yi oluşturmak için kullanılır ve durumu parametre olarak alır.

Durum değiştiğinde, yalnızca etkilenen Composable fonksiyonlar yeniden çalıştırılır ve sadece bu bileşenler güncellenir. Bu, UI’nin sadece değişen kısımlarının yeniden oluşturulması anlamına gelir ve performansı artırır.

Composable fonksiyonlar, UI bileşenlerinin her bir parçasını tanımlamak için kullanılır. Bu fonksiyonlar, bağımsız, yeniden kullanılabilir ve test edilebilir bileşenler oluşturmayı sağlar. Ayrıca, Jetpack Compose’un sunduğu araçlar sayesinde UI bileşenleri dinamik olarak güncellenebilir ve etkileşimli kullanıcı deneyimleri oluşturmak mümkün hale gelir. Bu şekilde, Jetpack Compose ile UI’nin oluşturulması ve güncellenmesi süreci daha basit, anlaşılır ve verimli hale gelir. Geliştiricilere daha esnek bir yaklaşım sunarak, daha hızlı ve daha etkili Android uygulamaları geliştirmelerine olanak sağlar.

State Hoisting nedir

State Hoisting, Jetpack Compose’da kullanılan bir tasarım desenidir. Daha sade, esnek ve test edilebilir bir kod yazma yaklaşımına yardımcı olur. Bu desen, bir bileşenin durumunu (state) ve bu durumu değiştiren işlevleri bir üst bileşene taşıma prensibine dayanır.

State Hoisting’in amacı, bileşenler arasındaki durumu paylaşarak veri akışını kolaylaştırmaktır. Bir bileşen içinde tanımlanan durum ve durumu değiştiren işlevler, bir üst bileşene taşınır ve bu bileşen tarafından yönetilir. Bu sayede, durumun tek bir kaynaktan güncellenmesi ve bileşenler arasında veri tutarlılığının sağlanması mümkün olur.

State Holder nedir

Modern bir kullanıcı arayüzü olan compose bildirim tabanlı bir UI modeline dayalı olarak çalışır ve State Holder’in güncellenmesiyle otomatik olarak UI’yi yeniler.

State Holder, Jetpack Compose’un içinde yer alan bir araçtır ve uygulama durumunu yönetmek için kullanılır. Bir State Holder, belirli bir veri türünü temsil eder ve bu veri türündeki değişiklikleri takip eder. Durum değiştiğinde, Jetpack Compose otomatik olarak UI’yi günceller, böylece herhangi bir manuel yenileme işlemine gerek kalmaz.

State Holder kullanmak için, remember fonksiyonunu kullanabiliriz. remember fonksiyonu, bir State Holder nesnesini oluşturur ve veriyi saklamak için kullanılır.

Makalenin sonraki kısımlarında kullanılıcak kod örneği :

val counterState = remember { mutableStateOf(CounterState(0)) }

Yukarıda ki kod parçacığı CounterState adında state holderimizi temsil eden bir data classımızın oldugunu ve başlangıç değerinin 0 oldugunu gösterir.

Ui State Implementasyonu

Öncelikle sayfanızın UI değişiklikleri için gereksinimlerinizi karşılayacak bir data class oluşturun

data class CounterState(val count : Int)

Sonrasında durumu takip etmesi ve güncellemesi için gerekli olan arka planda dönüştürme işlemlerini gerçekleştiren kodu oluşturun.

val counterState = remember { mutableStateOf(CounterState(0)) }

Holder statemize göre kendini güncelleyen bir composable yaratıyoruz

@Composable
fun CounterScreen(counterState: CounterState) {
Column {
Text(text = "Count: ${counterState.count}", fontSize = 24.sp)
Button(onClick = { /* Durumu güncelle */ }) {
Text(text = "Artır")
}
}
}

Yarattığımız buttonun basıldığında yani onClick fonksiyonunu tetiklediğimizde counterStatemizi güncelleyen kodu ekliyoruz.

Button(onClick = {
counterState.value = counterState.value.copy(count = counterState.value.count + 1)
}) {Text(text = "Artır")}

Bu şekilde basit bir implementasyon örneği gerçekleştirdik.

Yaşadığımız Sorun ve Çözüm

Projemizde ilk implemantasyonumuzda UiState değerini her ekran için ekrana özgü bir interface ve ona bağlı sınıflar ile yapmıştık. Ekranın durumunda değişikliğe sebep olacak durumlarda ilgili objeye yeni bir değer atıyorduk ve ekranın bu değere göre tepki vermesini sağlıyorduk.

UiState için bir interface oluşturulmuştu.

sealed interface MainActivityUiState {
object Empty MainActivityUiState
object Loading: MainActivityUiState
data class Success(val message: String): MainActivityUiState
data class Error(val exception: Throwable): MainActivityUiState
}

Bu interface viewmodel içinde observable yapısında çalışabilecek şekilde bir değişken olarak atanmıştı.

private val _uiState = MutableStateFlow<MainActivityUiState>(MainActivityUiState.Loading)
val uiState: StateFlow<MainActivityUiState> get() = _uiState

Encapsulation ile uiState değerinin dışarıdan değiştirilmesi engellenmişti. State değişikliğine ihtiyaç duyulduğunda (servis çağrısı sonrası gibi) ilgili _uiState değişkenine yeni değer ataması yapılmıştı.

_uiState.value = MainActivityUiState.Error(exception)

Ekran componentinde ise ilgili viewModel nesnesi oluşturulup UiState değeri aşağıdaki gibi lifecycle’la bağlı çalışacak bir state olarak dinlenilmeye başlandı.

@Composable
internal fun HomeRoute(
modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel())) {
val homeUiState by viewModel.uiState.collectAsStateWithLifecycle()
..
}

UiState değişimleri takip edilerek ekrandaki güncellemeleri yapıldı

when (mainActivityUiState) {
is MainActivityUiState.Loading -> {
.. // Show loading indicator
}
is MainActivityUiState.Success -> {
.. // Update UI
}
is MainActivityUiState. Error -> {
.. // Show error
}
}

Bu yaklaşım ile basit ekranları geliştirme konusunda bir problem yaşamadık. Fakat ekran ile ilgili data’nın birden çok servisten çekildiği veya ekranın parçalı yüklenmesini isteğimiz durumlarda bu yaklaşımı kullanmakta sıkıntı yaşadık. Çünkü UiState interface’sindeki her sınıf kendi başına ayrı bir durum belirtiyor ve ekranda korunması gereken bir durum olduğunda bu değer korunamıyor. (Success için bir çok farklı datanın gerekli olma durumu gibi düşünülebilir)

Ardından UiState mantığımızı daha esnek olacak şekilde güncellemeye karar verdik. UiState’i bir interface ve altındaki sınıflar olacak şekilde değil, tek bir sınıf ve içinde değişkenler olacak şekilde tasarladık.

data class LoginUiState(
val remainingTime: String,
val isLoading: Boolean,
val error: UiError?,
)

İlk implemantasyondaki gibi ViewModel içerisinde UiState tutan observable değişkenimizi tanımladık.

private val _uiState: MutableStateFlow<State> = MutableStateFlow(LoginUiState(
isLoading = true,
error = null,
remainingTime = “10:00”
)
)
val uiState: StateFlow<State> get() = _uiState

State değişimlerinde ise sınıfın bir kopyasını alıp sadece ilgili değerlerde güncelleme yaptık.

_uiState.update {
it.copy(remainingTime = "09:00")
}

Ekran gösterimini yapan composable yine aynı şekilde viewmodel üzerinden UiState’i observe ettik.

@Composable
internal fun LoginRoute(
modifier: Modifier = Modifier,
viewModel: LoginViewModel = hiltViewModel(),
) {
val loginUiState by viewModel.uiState.collectAsStateWithLifecycle()
..
}

State’in ekrandaki değişikliği yapabilmesi için component bazlı güncelleme yaptık.

Text(text = loginUiState.remainingTime)

Bu sayede ekranı ilgilendiren diğer dataları kaybetmeden sadece ilgili alanda değişiklik yapabilmiş olduk.

--

--