SharedFlow — isang mainit na reaktibong daloy mula sa library ng Kotlin Coroutines, na-optimize para sa isang beses na mga event (one-shot events) na hindi dapat maulit sa pag-ikot ng screen o muling paggawa ng subscriber. Ipinapakita namin ang pagkakaiba ng SharedFlow sa StateFlow: hindi tulad ng StateFlow, ang SharedFlow ay hindi nag-iimbak ng huling halaga para sa mga bagong subscriber at sumusuporta sa configuration ng replay, extraBufferCapacity at onBufferOverflow. Ayon sa Google (Android Developers, 2025), ang SharedFlow ay ang inirerekomendang solusyon para sa mga navigation command, Snackbar message at iba pang mga event na dapat iproseso nang eksaktong isang beses.
Mga Pangunahing Punto
SharedFlow — ay isang mainit na daloy (hot flow) mula sa library ng kotlinx.coroutines.flow na, hindi tulad ng StateFlow, ay hindi nakatali sa isang estado at maaaring mag-emit ng anumang bilang ng mga event sa anumang mga subscriber. Ang SharedFlow ay ang base type para sa StateFlow — ang StateFlow ay ipinatupad sa pamamagitan ng SharedFlow na may replay = 1.
Ang pangunahing katangian ng SharedFlow — hindi nito kailangang iimbak ang huling halaga. Bilang default (replay = 0), ang isang bagong subscriber ay hindi tumatanggap ng anuman hanggang sa maipadala ang isang bagong event. Ginagawa nitong perpekto ang SharedFlow para sa mga sitwasyon kung saan ang event ay dapat iproseso nang eksaktong isang beses: navigation, Snackbar, mga notification ng system, mga resulta ng pag-scan ng QR code.
Ang SharedFlow ay na-stabilize sa kotlinx.coroutines 1.4.0 (Nobyembre 2020) kasama ng StateFlow. Ayon sa dokumentasyon ng Kotlin Coroutines (2025), ang SharedFlow ay gumagamit ng fine-grained locking para sa pag-sync ng mga subscriber at nagbibigay ng linear scalability hanggang 1000+ sabay-sabay na subscriber nang walang pagbaba ng performance, na kinumpirma ng mga pagsubok ng JetBrains.
Ang pagpili sa pagitan ng SharedFlow at StateFlow ay depende sa semantika ng ipinapadalang data: estado (StateFlow) o event (SharedFlow). Nasa ibaba ang malinaw na mga pamantayan na may mga halimbawa.
| Pamantayan | SharedFlow | StateFlow |
|---|---|---|
| Semantika | Isang beses na mga event (navigation, toast, alert) | Estado ng UI (listahan, pag-load, error) |
| Paunang halaga | Hindi kinakailangan | Kinakailangan |
| Pag-uulit sa subscription | Lamang kung replay > 0 | Palaging huling halaga |
| Conflation | Hindi — hindi nawawala ang mga event (kung hindi puno ang buffer) | Oo — iniimbak lamang ang huli |
| Buffering | Nako-configure sa pamamagitan ng replay + extraBufferCapacity | Lamang 1 (replay=1 fixed) |
| Paggamit | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
Ang pinakasimpleng patakaran: kung ang data ay dapat ipakita sa pag-ikot ng screen — ito ay estado (StateFlow). Kung sa pag-ikot ng screen ang event ay hindi dapat maulit — ito ay isang beses na event (SharedFlow). Halimbawa, „toast na may mensahe ng error“ — SharedFlow: sa pag-ikot ay hindi dapat lumitaw muli ang toast. „Listahan ng mga produkto“ — StateFlow: sa pag-ikot ang listahan ay dapat manatili sa screen.
Sa IT Sectr ginagamit namin ang SharedFlow para sa: mga navigation command (paglipat sa screen, pagbubukas ng deep link), mga event ng UI (Snackbar, AlertDialog), mga notification ng system (pag-update ng data sa background, resulta ng pagbabayad), mga event ng analytics (pag-log, pag-track).
MutableSharedFlow — nababagong bersyon ng SharedFlow na may mga pamamaraang emit() (suspend) at tryEmit() (non-suspend) para sa pagpapadala ng mga event. Ang emit() ay nase-suspend kung puno ang buffer at onBufferOverflow = SUSPEND. Ang tryEmit() ay nagbabalik ng Boolean — kung ang event ay matagumpay na naidagdag sa 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
}
Ang mga parameter ng constructor ay kritikal: replay = 0 ay ginagarantiyang hindi mauulit ang event para sa isang bagong subscriber; extraBufferCapacity = 10 — buffer para sa mabilisang pagpapadala ng mga event bago mag-subscribe ang UI; DROP_OLDEST — strategy sa overflow: ang mga lumang event ay itinatapon, ang mga bago ay iniimbak. Ayon sa Kotlin Coroutines Performance (JetBrains, 2024), ang SharedFlow na may extraBufferCapacity = 64 ay nagpoproseso ng higit sa 100,000 event bawat segundo nang walang pagkawala.
Ang pattern ng Event (o UiEvent) — ang inirerekomendang paraan ng Google para sa pagpapadala ng isang beses na mga event mula ViewModel papunta sa View. Hindi tulad ng estado (StateFlow), ang event ay dapat iproseso nang eksaktong isang beses, at sa pag-ikot ng screen ay hindi dapat maulit. Ang SharedFlow na may replay = 0 ay perpekto para sa gawaing ito.
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 ?: "Error sa pag-format"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
Sa View (Activity/Fragment): ang subscription sa event ay dapat isagawa sa lifecycleScope na may repeatOnLifecycle(STATE.STARTED). Sa bawat pagpasok sa STARTED, ang subscription ay muling ginagawa, ngunit ang event ay hindi na uulit dahil ang SharedFlow na may replay=0 ay nailabas na ito. Ginagarantiyang nito na ang navigation sa screen ng pagsubaybay ng order ay magaganap nang isang beses lamang, hindi sa bawat pag-ikot.
Ang constructor ng MutableSharedFlow ay tumatanggap ng tatlong parameter na nagtatakda ng pag-uugali ng buffer. Ang maling configuration ay maaaring humantong sa pagkawala ng mga event o pag-block ng emit().
| Parameter | Uri | Default | Paglalarawan |
|---|---|---|---|
| replay | Int | 0 | Bilang ng mga huling event na nire-play para sa bagong subscriber. 0 = huwag i-replay, 1 = tulad ng StateFlow |
| extraBufferCapacity | Int | 0 | Karagdagang buffer lampas sa replay. Ang mga event ay iniimbak sa isang ring buffer. 64 — inirerekomendang limit para sa karamihan ng mga sitwasyon |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategy kapag puno ang buffer: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Mga configuration para sa iba't ibang sitwasyon:
// 1. Isang beses na UI event (navigation, toast)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Replay flow para sa pag-sync ng estado (tulad ng StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Mataas na dalas ng pagpapadala ng event (analytics, log)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Mahalaga: extraBufferCapacity + replay = kabuuang sukat ng buffer. Kung ang emit() ay tinatawag nang mas mabilis kaysa sa pagproseso ng subscriber ng event, mapupuno ang buffer at ma-t-trigger ang onBufferOverflow. Para sa mga event ng UI, ang DROP_OLDEST — ligtas na strategy: ang mga lumang event (mga navigation na hindi na nauugnay) ay itinatapon pabor sa mga bago. Para sa mga financial transaction, gamitin ang SUSPEND — ginagarantiyang nito na walang event ang mawawala sa halaga ng pag-block ng nagpadala.
Mga navigation command — klasikong use case para sa SharedFlow. Nag-subscribe ang Fragment sa mga event at nagsasagawa ng navigation. Sa pag-ikot ng screen, hindi nauulit ang command.
// 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
}
// Sa 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()
}
}
}
}
Komplikadong sitwasyon: SharedFlow para sa mga notification tungkol sa background event na pinagsama sa StateFlow para sa 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("Bagong notification: ${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("Lahat ng notification ay minarkahan bilang nabasa")
}
}
}
Sa halimbawang ito: Iniimbak ng StateFlow ang listahan ng mga notification (estado — nananatili sa pag-ikot), nag-e-emit ang SharedFlow ng mga toast message (isang beses na mga event — hindi nauulit sa pag-ikot). Ang kombinasyon ng dalawang uri ng Flow — ang inirerekomendang pattern ng Google para sa ViewModel mula noong 2022.
Mga Madalas Itanong
Oo, kung puno ang buffer at onBufferOverflow = DROP_OLDEST o DROP_LATEST. Hindi ginagarantiyahan ng SharedFlow ang paghahatid ng bawat event — hindi ito isang message queue (tulad ng Channel). Kung kailangan ang garantisadong paghahatid ng lahat ng event, gamitin ang Channel na may hindi umaapaw na buffer (UNLIMITED) o BroadcastChannel (deprecated). Para sa mga event ng UI, ang pagkawala ng mga lumang event (hal., lumang navigation) — ay inaasahang pag-uugali, hindi bug.
Channel — isang FIFO queue kung saan ang bawat event ay inihahatid sa eksaktong isang subscriber (point-to-point). SharedFlow — broadcast: ang bawat event ay inihahatid sa LAHAT ng aktibong subscriber. Ang SharedFlow ay mas malapit sa BroadcastChannel (na deprecated) at angkop para sa mga sitwasyong „isa-sa-marami“. Channel — para sa „isa-sa-isa“ (thread pools, pipeline). Ayon sa rekomendasyon ng JetBrains, ang SharedFlow ay kapalit ng BroadcastChannel para sa lahat ng bagong proyekto.
Ang SharedFlow ay thread-safe na — ang emit() at collect() ay wastong naka-sync. Maraming thread ang maaaring tumawag ng emit() nang walang pag-block, at lahat ng aktibong subscriber ay makakatanggap ng mga event sa tamang pagkakasunod-sunod. Ang tryEmit() ay hindi bumabara — nagbabalik ng false kung puno ang buffer. Para sa mga system na may mataas na load, gamitin ang tryEmit() na may DROP_OLDEST — pinipigilan nito ang pag-block ng mga thread.
Ang SharedFlow na walang replay=1 ay hindi nag-iimbak ng huling halaga — sa pag-ikot ng screen, ang bagong subscriber ay hindi makakatanggap ng kasalukuyang estado, mananatiling walang laman ang UI. Sa replay=1, ang SharedFlow ay kumikilos tulad ng StateFlow, ngunit nawawala ang optimization ng paghahambing sa pamamagitan ng equals(), na nagdudulot ng mga hindi kinakailangang notification sa muling pagpapadala ng parehong halaga. StateFlow — tamang pagpili para sa estado; SharedFlow — para sa mga event.
Para sa pag-test ng SharedFlow, gamitin ang Turbine — ang Kotlin library para sa pag-test ng Flow. Binibigyang-daan ka ng Turbine na suriin ang bawat emisyon nang hiwalay na may mga timeout at pag-verify ng pagkumpleto. Halimbawa: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Maaari mo ring gamitin ang .toList() sa runTest na may pagtukoy ng bilang ng inaasahang event.
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din