StateFlow — Kotlin Coroutines kütüphanesinden reaktif durum konteyneri olup, StateFlow<T> — her zaman güncel değeri tutan ve yeni abonelere bunu ileten Flow alt türüdür. StateFlow'un özünü anlatıyoruz: LiveData'dan farklı olarak StateFlow, Android çerçevesine bağlı değildir ve herhangi bir Kotlin platformunda çalışır. Google'a göre (Android Developers, 2025), StateFlow, özellikle Jetpack Compose ile MVVM mimarisinde, saf Kotlin ile yeni projeler için LiveData'ya ana alternatif olarak önerilmektedir.
Ana Noktalar
collectAsState() veya View'da repeatOnLifecycle() kullanılır.StateFlow — kotlinx.coroutines.flow kütüphanesinden, sabit replay = 1 parametresiyle MutableSharedFlow'u genişleten bir arayüzdür. Bu, StateFlow'un her zaman gönderilen son değeri hatırladığı ve her yeni aboneye hemen ilettiği anlamına gelir. LiveData'dan farklı olarak StateFlow, Kotlin Coroutines standart kütüphanesinin bir parçasıdır ve Android'e bağımlılığı yoktur.
Kavramsal olarak StateFlow reaktif bir özelliktir: geçerli değerini .value ile okur ve değişikliklere .collect() ile abone olursunuz. Bu modele "sıcak" akış (hot flow) denir — veri kaynağı, abonelerin varlığından bağımsız olarak aktiftir; flow { } ile oluşturulan ve bir abone göründüğünde başlatılan "soğuk" (cold) akışların aksine.
StateFlow, kotlinx.coroutines 1.3.7'de (Aralık 2020) stabilize edilmiş ve Google tarafından Google I/O 2021'den itibaren LiveData'nın yerine önerilmiştir. Ocak 2025 itibarıyla JetBrains anketine göre, yeni Kotlin Android projelerinin %56'sı ana reaktif konteyner olarak StateFlow kullanmaktadır.
StateFlow ve LiveData arasındaki seçim, proje mimarisine, teknoloji yığınına ve platform bağımsızlığı gereksinimlerine bağlıdır. Aşağıda altı temel kritere göre karşılaştırma bulunmaktadır.
| Kriter | StateFlow | LiveData |
|---|---|---|
| Platform | Kotlin Multiplatform (Android, iOS, sunucu) | Sadece Android |
| Lifecycle-aware | Hayır — repeatOnLifecycle() gerekli | Evet — yerleşik bağlantı |
| Coroutine'ler | Tam destek (map, filter, combine) | liveData { } builder aracılığıyla |
| Null-güvenliği | Evet — kotlinx.serialization ile serileştirilir | Evet — LiveData<String?> nullability ile |
| Conflation | Conflated — ara değerleri atlar | Sadece postValue() ile |
| Test etme | runTest + Turbine veya yerleşik operatörler | InstantTaskExecutorRule + observeForever |
StateFlow, View katmanında açık abonelik yönetimi gerektirir: Fragment/Activity'de abonelik repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } } ile yapılır. Bu, LiveData'nın otomatik aboneliğinden daha fazla kontrol sağlar, ancak kalıp kod ekler. Jetpack Compose'da abonelik val state by viewModel.uiState.collectAsState() ile basitleşir.
Google önerisi (Android Developers, 2025): Kotlin ile yeni projelerde, özellikle Compose ile çalışırken StateFlow kullanın. LiveData'yı şunlar için bırakın: (1) Java kodu, (2) Java ile uyumluluk gerektiren kütüphaneler, (3) Room DAO (DAO dönüş türü olarak LiveData hala popülerdir).
MutableStateFlow — yazma için açık value property'sine sahip değiştirilebilir StateFlow sürümüdür. MutableLiveData'ya benzer şekilde, MutableStateFlow ViewModel içinde kullanılır ve harici aboneler için StateFlow (salt okunur) olarak yayınlanır.
class TimerViewModel : ViewModel() {
private val _seconds = MutableStateFlow(0)
val seconds: StateFlow<Int> get() = _seconds
private val _isRunning = MutableStateFlow(false)
val isRunning: StateFlow<Boolean> get() = _isRunning
private var job: Job? = null
fun start() {
if (_isRunning.value) return
_isRunning.value = true
job = viewModelScope.launch {
while (_isRunning.value) {
delay(1000)
_seconds.value++
}
}
}
fun stop() {
_isRunning.value = false
job?.cancel()
}
}
MutableStateFlow özellikleri: (1) değer her zaman null değildir — kurucu aracılığıyla başlatma gerektirir; (2) eski ve yeni değerlerin equals() ile karşılaştırılması — yeni değer eskiyle eşitse, abonelere bildirim yapılmaz; (3) value'ya yazma herhangi bir akıştan mümkündür, ancak çağıran akışı yalnızca CAS işleminin kısa süresi boyunca bloklar. Kotlin Coroutines belgelerine göre, equals() ile karşılaştırma, LiveData'ya kıyasla gereksiz bildirimlerin sayısını %90 oranında azaltır — bu, yüksek güncelleme sıklığında performans artışı sağlar.
ViewModel'de StateFlow kullanırken şu kurallara uyun: (1) ViewModel içinde MutableStateFlow'u private değiştirici ile kullanın; (2) salt okunur StateFlow'u get() aracılığıyla yayınlayın; (3) karmaşık ekranlar için durum olarak sealed class kullanın; (4) mevcut değere eşit bir değer göndermekten kaçının (StateFlow bunu otomatik olarak yapar).
// Önerilen ekran durumu yapısı
sealed interface ProfileState {
data object Loading : ProfileState
data class Success(
val name: String,
val email: String,
val avatarUrl: String
) : ProfileState
data class Error(val message: String) : ProfileState
}
class ProfileViewModel : ViewModel() {
private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
val state: StateFlow<ProfileState> get() = _state
fun loadProfile(userId: String) {
viewModelScope.launch {
_state.value = ProfileState.Loading
try {
val profile = repository.getProfile(userId)
_state.value = ProfileState.Success(
name = profile.name,
email = profile.email,
avatarUrl = profile.avatarUrl
)
} catch (e: Exception) {
_state.value = ProfileState.Error(e.message ?: "Unknown error")
}
}
}
}
Tek tip durum olarak sealed class kullanımı, Google tarafından önerilen yaklaşımdır (UDF — Unidirectional Data Flow). UI'nın her zaman tutarlı bir durumda olmasını garanti eder: Loading, Success veya Error, ancak aynı anda değil. IT Sectr'da 2022'de tüm ekranlar için StateFlow + sealed class'a geçtik — bu, öngörülebilir durumlar sayesinde ViewModel testini %40 oranında basitleştirdi.
stateIn() — soğuk Flow'u sıcak StateFlow'a dönüştüren operatör. CoroutineScope (dahili coroutine'in başlatıldığı yer) ve SharingStarted stratejisinin belirtilmesini gerektirir. Doğru SharingStarted seçimi, StateFlow'un performansını ve yaşam döngüsünü kritik şekilde etkiler.
// Üç SharingStarted stratejisi:
// 1. SharingStarted.Eagerly — hemen başlatılır, durdurulmaz
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — ilk abonede başlatılır, durdurulmaz
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — aboneler varken başlatılır,
// son abone ayrıldıktan sonra stopTimeoutMillis (varsayılan 0) ile durdurulur
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) — ViewModel için en uygun strateji: son abone ayrıldıktan sonra dahili coroutine 5 saniye daha çalışmaya devam eder. Kullanıcı bu süre içinde ekrana dönerse, abonelik akışı yeniden başlatmadan geri yüklenir. Zaman aşımı, ekranlar arasında hızlı geçişlerde sık yeniden başlatmaları önler. Google testlerine göre (Android Performance, 2024), 5 saniyelik zaman aşımlı WhileSubscribed, CPU tüketimini Eagerly'ye kıyasla %25 oranında azaltır.
Arama sorgusu, sonuçlar ve yükleme durumu ile tam teşekküllü arama ekranı. ViewModel, Compose ile reaktif iletişim için sealed class UIState ve StateFlow kullanır.
sealed interface SearchUiState {
data object Empty : SearchUiState
data object Loading : SearchUiState
data class Results(val items: List<Product>) : SearchUiState
data class Error(val message: String) : SearchUiState
}
class SearchViewModel constructor(
private val repository: ProductRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> get() = _searchQuery
private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Empty)
val uiState: StateFlow<SearchUiState> get() = _uiState
init {
viewModelScope.launch {
_searchQuery
.debounce(300)
.filter { it.length >= 3 }
.flatMapLatest { query ->
_uiState.value = SearchUiState.Loading
repository.searchProducts(query)
}
.collect { products ->
_uiState.value = SearchUiState.Results(products)
}
}
}
fun onQueryChanged(query: String) {
_searchQuery.value = query
}
}
// Compose'da:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// ... Loading, Results, Error durumlarına tepki veren UI
}
Room (sürüm 2.4.0'dan itibaren) DAO'dan Flow dönüşünü destekler. Birden çok Flow'u combine ile birleştirmek, karmaşık ekranlar için güçlü bir desendir.
@Dao
interface OrderDao {
@Query("SELECT * FROM orders WHERE status = :status")
fun getOrdersByStatus(status: String): Flow<List<Order>>
}
class OrderViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDatabase.getDatabase(application).orderDao()
val activeOrders: StateFlow<List<Order>> = dao.getOrdersByStatus("active")
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val summary: StateFlow<OrderSummary> = combine(
dao.getOrdersByStatus("active"),
dao.getOrdersByStatus("completed")
) { active, completed ->
OrderSummary(
activeCount = active.size,
completedCount = completed.size,
totalAmount = (active + completed).sumOf { it.amount }
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrderSummary(0, 0, 0.0))
}
Room, orders tablosundaki değişiklikleri otomatik olarak izler ve herhangi bir değişiklikte verileri yeniden sorgular. StateFlow + Room — Room + LiveData ikilisinin modern yerine geçendir. Google'a göre (Android Architecture Guide, 2025), veritabanı değişikliklerinde reaktif UI güncellemesi gerektiren tüm Kotlin projelerinde Flow + StateFlow + Room ikilisi önerilmektedir.
Sıkça Sorulan Sorular
Conflasyon — StateFlow'un yalnızca gönderilen son değeri sakladığı mekanizmadır. Yeni bir değer, abone öncekini işlemeden gönderilirse, ara değer kaybolur. Bu UI için önemlidir: durum Loading → Success → Error olarak değişirse ve UI Success'i çizmeye fırsat bulamazsa, gereksiz render olmadan doğrudan Error'a geçer. Conflasyon, Compose'da gereksiz yeniden birleştirmeleri önleyen temel bir Android optimizasyonudur.
lifecycle-livedata-ktx kütüphanesinden liveData.asFlow() extension fonksiyonunu, ardından StateFlow'a dönüştürmek için .stateIn() kullanın. Ters dönüşüm — stateFlow.asLiveData(). Dönüşüm, LiveData'dan StateFlow'a geçişte faydalıdır: ViewModel'leri kademeli olarak StateFlow'a taşıyabilir, eski View'ın LiveData üzerinden aboneliğini bırakabilirsiniz.
StateFlow her zaman bir değere sahip olmalıdır — bu arayüzün sözleşmesidir: yeni bağlanan her abone, beklemeden mevcut durumu hemen alır. Başlangıç değeri MutableStateFlow(initialValue) kurucusuna veya stateIn(initialValue) operatörüne iletilir. Durum mevcut olmayabilirse, nullable tür ile MutableStateFlow<T?>(null) kullanın ve UI'da null'u işleyin.
Evet, StateFlow thread-safe'dir: value yazma ve okuma atomik işlemler (CAS) kullanır. Ancak collect() bir suspend fonksiyonudur ve bir coroutine içinde başlatılmalıdır. Emission ve collect farklı akışlarda yürütülürse, StateFlow tüm value işlemleri için happens-before garantisi verir. View'da StateFlow toplamak için lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } } kullanın.
Herhangi bir sınırlama yoktur, ancak ekran başına 3-5'ten fazla ayrı StateFlow önerilmez. Daha fazla farklı durum gerekiyorsa, bunları sealed class veya data class aracılığıyla birleştirin. Her StateFlow, toplama sırasında bir Continuation nesnesinin ayrılmasını gerektirir — yüz StateFlow, GC üzerinde gözle görülür bir yük oluşturabilir. Google önerisine göre, ekran başına bir sealed class UIState, okunabilirlik ve performans arasında optimal dengedir.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun