SharedFlow — aliran reaktif panas dari pustaka Kotlin Coroutines, dioptimalkan untuk event satu kali (one-shot events) yang tidak boleh berulang saat rotasi layar atau pembuatan ulang pelanggan. Kami menunjukkan perbedaan SharedFlow dengan StateFlow: tidak seperti StateFlow, SharedFlow tidak menyimpan nilai terakhir untuk pelanggan baru dan mendukung konfigurasi replay, extraBufferCapacity, dan onBufferOverflow. Menurut Google (Android Developers, 2025), SharedFlow adalah solusi yang direkomendasikan untuk perintah navigasi, pesan Snackbar, dan event lain yang harus diproses tepat satu kali.
Poin Utama
SharedFlow — adalah aliran panas (hot flow) dari pustaka kotlinx.coroutines.flow yang, tidak seperti StateFlow, tidak terikat pada satu status dan dapat mengirimkan sejumlah event ke pelanggan mana pun. SharedFlow adalah tipe dasar untuk StateFlow — StateFlow diimplementasikan melalui SharedFlow dengan replay = 1.
Fitur utama SharedFlow — tidak wajib menyimpan nilai terakhir. Secara default (replay = 0), pelanggan baru tidak menerima apa pun sampai event baru dikirim. Ini membuat SharedFlow ideal untuk skenario di mana event harus diproses tepat satu kali: navigasi, Snackbar, notifikasi sistem, hasil pemindaian kode QR.
SharedFlow distabilkan di kotlinx.coroutines 1.4.0 (November 2020) bersama dengan StateFlow. Menurut dokumentasi Kotlin Coroutines (2025), SharedFlow menggunakan penguncian granularitas halus untuk sinkronisasi pelanggan dan memberikan skalabilitas linier hingga 1000+ pelanggan simultan tanpa degradasi kinerja, yang dikonfirmasi oleh pengujian JetBrains.
Pilihan antara SharedFlow dan StateFlow tergantung pada semantik data yang dikirimkan: status (StateFlow) atau event (SharedFlow). Di bawah ini adalah kriteria yang jelas dengan contoh.
| Kriteria | SharedFlow | StateFlow |
|---|---|---|
| Semantik | Event satu kali (navigasi, toast, peringatan) | Status UI (daftar, pemuatan, kesalahan) |
| Nilai awal | Tidak diperlukan | Wajib |
| Pengulangan saat berlangganan | Hanya jika replay > 0 | Selalu nilai terakhir |
| Konflasi | Tidak — event tidak hilang (jika buffer tidak penuh) | Ya — hanya menyimpan yang terakhir |
| Buffering | Dapat dikonfigurasi melalui replay + extraBufferCapacity | Hanya 1 (replay=1 tetap) |
| Penggunaan | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
Aturan paling sederhana: jika data harus ditampilkan saat rotasi layar — ini status (StateFlow). Jika saat rotasi layar event tidak boleh berulang — ini event satu kali (SharedFlow). Misalnya, „toast dengan pesan kesalahan“ — SharedFlow: saat rotasi toast tidak boleh muncul lagi. „Daftar produk“ — StateFlow: saat rotasi daftar harus tetap di layar.
Di IT Sectr kami menggunakan SharedFlow untuk: perintah navigasi (pindah ke layar, membuka deep link), event UI (Snackbar, AlertDialog), notifikasi sistem (pembaruan data latar belakang, hasil pembayaran), event analitik (logging, pelacakan).
MutableSharedFlow — versi SharedFlow yang dapat diubah dengan metode emit() (suspend) dan tryEmit() (non-suspend) untuk mengirim event. emit() akan menangguhkan jika buffer penuh dan onBufferOverflow = SUSPEND. tryEmit() mengembalikan Boolean — apakah event berhasil ditambahkan ke buffer.
class EventBus {
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<UiEvent> get() = _events
suspend fun sendEvent(event: UiEvent) {
_events.emit(event)
}
fun trySendEvent(event: UiEvent): Boolean {
return _events.tryEmit(event)
}
}
sealed interface UiEvent {
data class ShowSnackbar(val message: String) : UiEvent
data class NavigateTo(val route: String) : UiEvent
data class ShowDialog(val title: String, val message: String) : UiEvent
}
Parameter konstruktor sangat penting: replay = 0 memastikan event tidak berulang untuk pelanggan baru; extraBufferCapacity = 10 — buffer untuk pengiriman event cepat sebelum UI berlangganan; DROP_OLDEST — strategi saat overflow: event lama dibuang, yang baru disimpan. Menurut Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow dengan extraBufferCapacity = 64 memproses lebih dari 100.000 event per detik tanpa kehilangan.
Pola Event (atau UiEvent) — cara yang direkomendasikan Google untuk mengirimkan event satu kali dari ViewModel ke View. Tidak seperti status (StateFlow), event harus diproses tepat satu kali, dan saat rotasi layar tidak boleh berulang. SharedFlow dengan replay = 0 sangat ideal untuk tugas ini.
class CheckoutViewModel : ViewModel() {
private val _uiState = MutableStateFlow<CheckoutState>(CheckoutState.Idle)
val uiState: StateFlow<CheckoutState> get() = _uiState
private val _event = MutableSharedFlow<CheckoutEvent>()
val event: SharedFlow<CheckoutEvent> get() = _event
fun placeOrder() {
viewModelScope.launch {
_uiState.value = CheckoutState.Loading
try {
val orderId = orderRepository.createOrder(cart)
_uiState.value = CheckoutState.Success(orderId)
_event.emit(CheckoutEvent.NavigateToOrderTracking(orderId))
} catch (e: Exception) {
_uiState.value = CheckoutState.Error(e.message)
_event.emit(CheckoutEvent.ShowErrorSnackbar(e.message ?: "Kesalahan format"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
Di View (Activity/Fragment): langganan event harus dilakukan di lifecycleScope dengan repeatOnLifecycle(STATE.STARTED). Setiap kali masuk ke STARTED, langganan dibuat ulang, tetapi event tidak berulang karena SharedFlow dengan replay=0 telah melepaskannya. Ini memastikan bahwa navigasi ke layar pelacakan pesanan hanya terjadi satu kali, bukan setiap rotasi.
Konstruktor MutableSharedFlow menerima tiga parameter yang menentukan perilaku buffer. Konfigurasi yang salah dapat menyebabkan hilangnya event atau pemblokiran emit().
| Parameter | Tipe | Default | Deskripsi |
|---|---|---|---|
| replay | Int | 0 | Jumlah event terakhir yang diputar ulang ke pelanggan baru. 0 = jangan putar ulang, 1 = seperti StateFlow |
| extraBufferCapacity | Int | 0 | Buffer tambahan di luar replay. Event disimpan dalam buffer melingkar. 64 — batas yang direkomendasikan untuk sebagian besar skenario |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategi saat buffer penuh: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Konfigurasi untuk berbagai skenario:
// 1. Event UI satu kali (navigasi, toast)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Aliran replay untuk sinkronisasi status (seperti StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Pengiriman event frekuensi tinggi (analitik, log)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Penting: extraBufferCapacity + replay = ukuran total buffer. Jika emit() dipanggil lebih cepat daripada pelanggan memproses event, buffer akan penuh dan onBufferOverflow akan terpicu. Untuk event UI, DROP_OLDEST — strategi aman: event lama (navigasi yang sudah tidak relevan) dibuang demi yang baru. Untuk transaksi keuangan, gunakan SUSPEND — ini memastikan tidak ada event yang hilang dengan mengorbankan pemblokiran pengirim.
Perintah navigasi — use case klasik untuk SharedFlow. Fragment berlangganan event dan menjalankan navigasi. Saat rotasi layar, perintah tidak berulang.
// ViewModel
class AuthViewModel : ViewModel() {
private val _navEvent = MutableSharedFlow<NavEvent>()
val navEvent: SharedFlow<NavEvent> get() = _navEvent
fun onLoginSuccess() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.HOME))
}
}
fun onLogout() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.LOGIN))
}
}
}
sealed interface NavEvent {
data class NavigateTo(val route: String) : NavEvent
data class NavigateBack(val popUpTo: String? = null) : NavEvent
}
// Di Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navEvent.collect { navEvent ->
when (navEvent) {
is NavEvent.NavigateTo -> findNavController().navigate(navEvent.route)
is NavEvent.NavigateBack -> findNavController().popBackStack()
}
}
}
}
Skenario kompleks: SharedFlow untuk notifikasi tentang event latar belakang dengan menggabungkan StateFlow untuk UI.
class NotificationViewModel : ViewModel() {
private val _toastMessage = MutableSharedFlow<String>()
val toastMessage: SharedFlow<String> get() = _toastMessage
private val _notifications = MutableStateFlow<List<Notification>>(emptyList())
val notifications: StateFlow<List<Notification>> get() = _notifications
init {
viewModelScope.launch {
notificationChannel
.consumeAsFlow()
.collect { notification ->
_notifications.value = _notifications.value + notification
_toastMessage.emit("Notifikasi baru: ${notification.title}")
}
}
}
fun dismissNotification(id: String) {
_notifications.value = _notifications.value.filter { it.id != id }
}
fun markAllRead() {
viewModelScope.launch {
_notifications.value = _notifications.value.map { it.copy(isRead = true) }
_toastMessage.emit("Semua notifikasi ditandai sebagai telah dibaca")
}
}
}
Dalam contoh ini: StateFlow menyimpan daftar notifikasi (status — dipertahankan saat rotasi), SharedFlow mengirim pesan toast (event satu kali — tidak berulang saat rotasi). Kombinasi dua tipe Flow — pola yang direkomendasikan Google untuk ViewModel sejak 2022.
Pertanyaan yang Sering Diajukan
Ya, jika buffer penuh dan onBufferOverflow = DROP_OLDEST atau DROP_LATEST. SharedFlow tidak menjamin pengiriman setiap event — ini bukan antrian pesan (seperti Channel). Jika diperlukan pengiriman terjamin semua event, gunakan Channel dengan buffer tidak terbatas (UNLIMITED) atau BroadcastChannel (deprecated). Untuk event UI, hilangnya event usang (misalnya, navigasi lama) — adalah perilaku yang diharapkan, bukan bug.
Channel — antrian FIFO di mana setiap event dikirimkan ke tepat satu pelanggan (titik-ke-titik). SharedFlow — siaran: setiap event dikirimkan ke SEMUA pelanggan aktif. SharedFlow lebih dekat ke BroadcastChannel (yang sudah deprecated) dan cocok untuk skenario „satu-ke-banyak“. Channel — untuk „satu-ke-satu“ (pool thread, pipeline). Menurut rekomendasi JetBrains, SharedFlow adalah pengganti BroadcastChannel untuk semua proyek baru.
SharedFlow sudah thread-safe — emit() dan collect() tersinkronisasi dengan benar. Banyak thread dapat memanggil emit() tanpa pemblokiran, dan semua pelanggan aktif akan menerima event dalam urutan yang benar. tryEmit() tidak memblokir — mengembalikan false jika buffer penuh. Untuk sistem dengan beban tinggi, gunakan tryEmit() dengan DROP_OLDEST — ini mencegah pemblokiran thread.
SharedFlow tanpa replay=1 tidak menyimpan nilai terakhir — saat rotasi layar, pelanggan baru tidak menerima status saat ini, UI tetap kosong. Dengan replay=1, SharedFlow berperilaku seperti StateFlow, tetapi kehilangan optimasi perbandingan melalui equals(), yang menyebabkan notifikasi tidak perlu saat mengirim ulang nilai yang sama. StateFlow — pilihan tepat untuk status; SharedFlow — untuk event.
Untuk menguji SharedFlow, gunakan Turbine — pustaka Kotlin untuk menguji Flow. Turbine memungkinkan Anda memeriksa setiap emisi secara terpisah dengan batas waktu dan verifikasi penyelesaian. Contoh: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Anda juga dapat menggunakan .toList() di runTest dengan menentukan jumlah event yang diharapkan.
Ringkasan
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