StateFlow — wadah state reaktif dari pustaka Kotlin Coroutines, yang mewakili StateFlow<T> — subtipe Flow yang selalu menyimpan nilai terkini dan mengeluarkannya ke pelanggan baru. Menjelaskan esensi StateFlow: tidak seperti LiveData, StateFlow tidak terikat pada framework Android dan bekerja di platform Kotlin mana pun. Menurut Google (Android Developers, 2025), StateFlow direkomendasikan sebagai alternatif utama LiveData untuk proyek baru di Kotlin murni, terutama dalam arsitektur MVVM dengan Jetpack Compose.
Poin Utama
collectAsState() di Compose atau repeatOnLifecycle() di View.StateFlow adalah antarmuka dari pustaka kotlinx.coroutines.flow yang memperluas MutableSharedFlow dengan parameter tetap replay = 1. Ini berarti StateFlow selalu mengingat nilai terakhir yang dikirim dan segera memutarnya untuk setiap pelanggan baru. Tidak seperti LiveData, StateFlow adalah bagian dari pustaka standar Kotlin Coroutines dan tidak memiliki ketergantungan pada Android.
Secara konseptual, StateFlow adalah properti reaktif: Anda membaca nilai saat ini melalui .value dan berlangganan perubahan melalui .collect(). Model ini disebut aliran «panas» (hot flow) — sumber data aktif terlepas dari keberadaan pelanggan, berbeda dengan aliran «dingin» (cold) yang dibuat melalui flow { } yang dimulai saat pelanggan muncul.
StateFlow distabilkan di kotlinx.coroutines 1.3.7 (Desember 2020) dan direkomendasikan oleh Google sebagai pengganti LiveData mulai Google I/O 2021. Hingga Januari 2025, menurut survei JetBrains, 56% proyek Android baru di Kotlin menggunakan StateFlow sebagai wadah reaktif utama.
Pilihan antara StateFlow dan LiveData tergantung pada arsitektur proyek, tumpukan teknologi, dan persyaratan independensi platform. Di bawah ini — perbandingan berdasarkan enam kriteria utama.
| Kriteria | StateFlow | LiveData |
|---|---|---|
| Platform | Kotlin Multiplatform (Android, iOS, server) | Hanya Android |
| Lifecycle-aware | Tidak — memerlukan repeatOnLifecycle() | Ya — pengikatan bawaan |
| Coroutine | Dukungan penuh (map, filter, combine) | Melalui liveData { } builder |
| Keamanan null | Ya — diserialisasi melalui kotlinx.serialization | Ya — melalui nullability LiveData<String?> |
| Konflasi | Conflated — melewatkan nilai antara | Hanya melalui postValue() |
| Pengujian | runTest + Turbine atau operator bawaan | InstantTaskExecutorRule + observeForever |
StateFlow memerlukan manajemen langganan eksplisit di lapisan View: di Fragment/Activity langganan dilakukan melalui repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. Ini memberikan kontrol lebih daripada langganan otomatis LiveData, tetapi menambahkan kode boilerplate. Di Jetpack Compose langganan disederhanakan menjadi val state by viewModel.uiState.collectAsState().
Rekomendasi Google (Android Developers, 2025): untuk proyek baru di Kotlin gunakan StateFlow, terutama saat bekerja dengan Compose. Pertahankan LiveData untuk: (1) kode Java, (2) pustaka yang memerlukan kompatibilitas Java, (3) Room DAO (LiveData sebagai tipe kembalian DAO masih populer).
MutableStateFlow — versi StateFlow yang dapat diubah dengan properti value terbuka untuk ditulis. Analog dengan MutableLiveData, MutableStateFlow digunakan di dalam ViewModel dan dipublikasikan sebagai StateFlow (hanya baca) untuk pelanggan eksternal.
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()
}
}
Karakteristik MutableStateFlow: (1) nilai selalu non-null — memerlukan inisialisasi melalui konstruktor; (2) perbandingan nilai lama dan baru melalui equals() — jika nilai baru sama dengan yang lama, pelanggan TIDAK diberitahu; (3) penulisan ke value dimungkinkan dari thread mana pun, tetapi memblokir thread pemanggil hanya untuk durasi singkat operasi CAS. Menurut dokumentasi Kotlin Coroutines, perbandingan melalui equals() mengurangi jumlah pemberitahuan yang tidak perlu sebesar 90% dibandingkan dengan LiveData — ini memberikan peningkatan kinerja pada frekuensi pembaruan tinggi.
Saat menggunakan StateFlow di ViewModel ikuti aturan berikut: (1) gunakan MutableStateFlow dengan modifier private di dalam ViewModel; (2) publikasikan StateFlow hanya baca melalui get(); (3) untuk layar kompleks gunakan sealed class sebagai state; (4) hindari mengeluarkan nilai yang sama dengan saat ini (StateFlow melakukannya secara otomatis).
// Struktur state layar yang direkomendasikan
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")
}
}
}
}
Penggunaan sealed class sebagai tipe state tunggal — pendekatan yang direkomendasikan Google (UDF — Unidirectional Data Flow). Ini menjamin bahwa UI selalu berada dalam state yang konsisten: Loading, Success, atau Error, tetapi tidak secara bersamaan. Di IT Sectr kami beralih ke StateFlow + sealed class untuk semua layar pada tahun 2022 — ini menyederhanakan pengujian ViewModel sebesar 40% berkat state yang dapat diprediksi.
stateIn() — operator yang mengubah Flow dingin menjadi StateFlow panas. Memerlukan penentuan CoroutineScope (tempat coroutine internal dimulai) dan strategi SharingStarted. Pemilihan SharingStarted yang tepat secara kritis memengaruhi kinerja dan siklus hidup StateFlow.
// Tiga strategi SharingStarted:
// 1. SharingStarted.Eagerly — dimulai segera, tidak berhenti
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — dimulai pada pelanggan pertama, tidak berhenti
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — dimulai saat ada pelanggan,
// berhenti setelah stopTimeoutMillis (default 0) setelah pelanggan terakhir pergi
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) — strategi optimal untuk ViewModel: setelah pelanggan terakhir pergi, coroutine internal terus bekerja selama 5 detik lagi. Jika pengguna kembali ke layar dalam waktu ini, langganan dipulihkan tanpa memulai ulang aliran. Batas waktu mencegah restart yang sering saat perpindahan cepat antar layar. Menurut pengujian Google (Android Performance, 2024), WhileSubscribed dengan batas waktu 5 detik mengurangi konsumsi CPU sebesar 25% dibandingkan dengan Eagerly.
Layar pencarian lengkap dengan kueri, hasil, dan state pemuatan. ViewModel menggunakan sealed class UIState dan StateFlow untuk komunikasi reaktif dengan Compose.
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
}
}
// Di Compose:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// ... UI yang bereaksi terhadap state Loading, Results, Error
}
Room (mulai versi 2.4.0) mendukung pengembalian Flow dari DAO. Menggabungkan beberapa Flow melalui combine adalah pola yang kuat untuk layar kompleks.
@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 secara otomatis melacak perubahan di tabel orders dan meminta ulang data pada setiap perubahan. StateFlow + Room — pengganti modern untuk pasangan Room + LiveData. Menurut Google (Android Architecture Guide, 2025), kombinasi Flow + StateFlow + Room direkomendasikan untuk semua proyek Kotlin yang memerlukan pembaruan UI reaktif saat perubahan basis data.
Pertanyaan yang Sering Diajukan
Konflasi — mekanisme di mana StateFlow hanya menyimpan nilai terakhir yang dikirim. Jika nilai baru dikirim sebelum pelanggan memproses yang sebelumnya, nilai antara akan hilang. Ini penting untuk UI: jika state berubah dari Loading → Success → Error, dan UI belum sempat merender Success, ia langsung beralih ke Error tanpa rendering yang tidak perlu. Konflasi adalah optimasi kunci Android yang mencegah rekonsiliasi berlebihan di Compose.
Gunakan fungsi ekstensi liveData.asFlow() dari pustaka lifecycle-livedata-ktx, lalu .stateIn() untuk konversi ke StateFlow. Konversi terbalik — stateFlow.asLiveData(). Konversi berguna saat migrasi dari LiveData ke StateFlow: Anda dapat secara bertahap memindahkan ViewModel ke StateFlow, mempertahankan langganan View lama melalui LiveData.
StateFlow harus selalu memiliki nilai — ini adalah kontrak antarmuka: setiap pelanggan yang baru terhubung segera menerima state saat ini tanpa menunggu. Nilai awal diteruskan ke konstruktor MutableStateFlow(initialValue) atau ke operator stateIn(initialValue). Jika state mungkin tidak ada, gunakan MutableStateFlow<T?>(null) dengan tipe nullable dan tangani null di UI.
Ya, StateFlow thread-safe: penulisan dan pembacaan value menggunakan operasi atomik (CAS). Namun, collect() adalah fungsi suspend dan harus dijalankan di coroutine. Jika emisi dan collect dijalankan di thread yang berbeda, StateFlow menjamin happens-before untuk semua operasi pada value. Untuk mengumpulkan StateFlow di View, gunakan lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.
Tidak ada batasan, tetapi disarankan tidak lebih dari 3-5 StateFlow terpisah per layar. Jika diperlukan lebih banyak state berbeda, gabungkan menjadi satu melalui sealed class atau data class. Setiap StateFlow memerlukan alokasi objek Continuation saat pengumpulan — seratus StateFlow dapat menciptakan beban yang nyata bagi GC. Menurut rekomendasi Google, satu sealed class UIState per layar adalah keseimbangan optimal antara keterbacaan dan kinerja.
Kesimpulan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga