SharedFlow é um fluxo reativo quente da biblioteca Kotlin Coroutines, otimizado para eventos únicos (one-shot events) que não devem se repetir ao girar a tela ou recriar o assinante. Mostramos como o SharedFlow difere do StateFlow: ao contrário do StateFlow, o SharedFlow não armazena o último valor para novos assinantes e suporta configuração de replay, extraBufferCapacity e onBufferOverflow. De acordo com o Google (Android Developers, 2025), o SharedFlow é a solução recomendada para comandos de navegação, mensagens Snackbar e outros eventos que devem ser processados exatamente uma vez.
Pontos Principais
SharedFlow é um fluxo quente (hot flow) da biblioteca kotlinx.coroutines.flow que, ao contrário do StateFlow, não está vinculado a um único estado e pode emitir um número arbitrário de eventos para assinantes arbitrários. SharedFlow é o tipo base para StateFlow — StateFlow é implementado através de SharedFlow com replay = 1.
A principal característica do SharedFlow é que ele não precisa armazenar o último valor. Por padrão (replay = 0), um novo assinante não recebe nada até que um novo evento seja enviado. Isso torna o SharedFlow ideal para cenários onde um evento deve ser processado exatamente uma vez: navegação, Snackbar, notificações do sistema, resultados de digitalização de QR code.
O SharedFlow foi estabilizado no kotlinx.coroutines 1.4.0 (novembro de 2020) junto com o StateFlow. De acordo com a documentação do Kotlin Coroutines (2025), o SharedFlow usa bloqueio de granulação fina para sincronização de assinantes e fornece escalabilidade linear para mais de 1000 assinantes simultâneos sem degradação de desempenho, confirmado por testes da JetBrains.
A escolha entre SharedFlow e StateFlow depende da semântica dos dados transferidos: estado (StateFlow) ou evento (SharedFlow). Abaixo estão critérios claros com exemplos.
| Critério | SharedFlow | StateFlow |
|---|---|---|
| Semântica | Eventos únicos (navegação, toast, alerta) | Estado da UI (lista, carregamento, erro) |
| Valor inicial | Não obrigatório | Obrigatório |
| Reprodução na assinatura | Apenas se replay > 0 | Sempre o último valor |
| Conflação | Não — eventos não são perdidos (se o buffer não estiver cheio) | Sim — armazena apenas o último |
| Bufferização | Configurável via replay + extraBufferCapacity | Apenas 1 (replay=1 fixo) |
| Uso | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
A regra mais simples: se os dados devem ser mostrados ao girar a tela — é estado (StateFlow). Se ao girar a tela o evento não deve se repetir — é um evento único (SharedFlow). Por exemplo, um toast com mensagem de erro é SharedFlow: ao girar, o toast não deve aparecer novamente. Uma lista de produtos é StateFlow: ao girar, a lista deve permanecer na tela.
Na IT Sectr usamos SharedFlow para: comandos de navegação (transição de tela, abertura de deep link), eventos de UI (Snackbar, AlertDialog), notificações do sistema (atualizações de dados em segundo plano, resultado de pagamento), eventos de análise (registro, rastreamento).
MutableSharedFlow é a versão mutável do SharedFlow com métodos emit() (suspend) e tryEmit() (não suspend) para envio de eventos. emit() suspende se o buffer estiver cheio e onBufferOverflow = SUSPEND. tryEmit() retorna um Boolean indicando se o evento foi adicionado com sucesso ao 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
}
Os parâmetros do construtor são criticamente importantes: replay = 0 garante que o evento não se repita para um novo assinante; extraBufferCapacity = 10 fornece um buffer para emissão rápida de eventos antes que a UI assine; DROP_OLDEST é a estratégia de estouro: eventos antigos são descartados, novos são preservados. De acordo com Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow com extraBufferCapacity = 64 processa mais de 100.000 eventos por segundo sem perdas.
Padrão Event (ou UiEvent) é a forma recomendada pelo Google para passar eventos únicos de ViewModel para View. Ao contrário do estado (StateFlow), um evento deve ser processado exatamente uma vez e, ao girar a tela, não deve se repetir. SharedFlow com replay = 0 é ideal para esta tarefa.
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 ?: "Erro de layout"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
Na View (Activity/Fragment): a assinatura de eventos deve ser feita em lifecycleScope com repeatOnLifecycle(STATE.STARTED). Em cada entrada em STARTED, a assinatura é recriada, mas o evento não se repete porque o SharedFlow com replay=0 já o liberou. Isso garante que a navegação para a tela de rastreamento de pedidos ocorra apenas uma vez, não a cada rotação.
O construtor do MutableSharedFlow aceita três parâmetros que determinam o comportamento do buffer. Configuração incorreta pode levar à perda de eventos ou bloqueio de emit().
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
| replay | Int | 0 | Número de eventos recentes reproduzidos para um novo assinante. 0 = não reproduzir, 1 = como StateFlow |
| extraBufferCapacity | Int | 0 | Buffer adicional além do replay. Eventos são armazenados em um buffer circular. 64 é o limite recomendado para a maioria dos cenários |
| onBufferOverflow | BufferOverflow | SUSPEND | Estratégia quando o buffer está cheio: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Configurações para diferentes cenários:
// 1. Eventos de UI únicos (navegação, toasts)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Fluxo de replay para sincronização de estado (como StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Emissão de eventos de alta frequência (análise, registros)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Importante: extraBufferCapacity + replay = tamanho total do buffer. Se emit() for chamado mais rápido do que o assinante processa os eventos, o buffer enche e onBufferOverflow é acionado. Para eventos de UI, DROP_OLDEST é uma estratégia segura: eventos antigos (navegações não mais relevantes) são descartados em favor dos novos. Para transações financeiras, use SUSPEND — isso garante que nenhum evento seja perdido ao custo de bloquear o remetente.
Comandos de navegação são um caso de uso clássico para SharedFlow. Um Fragment assina os eventos e realiza a navegação. Ao girar a tela, o comando não se repete.
// 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
}
// No 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()
}
}
}
}
Um cenário complexo: SharedFlow para notificações de eventos em segundo plano combinado com StateFlow para a 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("Nova notificação: ${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("Todas as notificações marcadas como lidas")
}
}
}
Neste exemplo: StateFlow armazena a lista de notificações (estado — preservado na rotação), SharedFlow emite mensagens toast (eventos únicos — não repetidos na rotação). A combinação de dois tipos de Flow é o padrão recomendado pelo Google para ViewModel a partir de 2022.
Perguntas Frequentes
Sim, se o buffer estiver cheio e onBufferOverflow = DROP_OLDEST ou DROP_LATEST. SharedFlow não garante a entrega de cada evento — não é uma fila de mensagens (como Channel). Se precisar de entrega garantida de todos os eventos, use Channel com buffer ilimitado (UNLIMITED) ou BroadcastChannel (obsoleto). Para eventos de UI, a perda de eventos obsoletos (por exemplo, navegação antiga) é comportamento esperado, não um bug.
Channel é uma fila FIFO onde cada evento é entregue a exatamente um assinante (ponto a ponto). SharedFlow é uma transmissão: cada evento é entregue a TODOS os assinantes ativos. SharedFlow é mais próximo de BroadcastChannel (que está obsoleto) e é adequado para cenários de um-para-muitos. Channel é para um-para-um (pools de threads, pipelines). De acordo com a recomendação da JetBrains, SharedFlow é o substituto do BroadcastChannel em todos os novos projetos.
SharedFlow já é seguro para threads — emit() e collect() estão devidamente sincronizados. Várias threads podem chamar emit() sem bloqueios, e todos os assinantes ativos recebem os eventos na ordem correta. tryEmit() não é bloqueante — retorna false se o buffer estiver cheio. Para sistemas de alta carga, use tryEmit() com DROP_OLDEST — isso evita o bloqueio de threads.
SharedFlow sem replay=1 não armazena o último valor — ao girar a tela, um novo assinante não receberá o estado atual e a UI ficará vazia. Com replay=1, SharedFlow se comporta como StateFlow, mas perde a otimização de comparação via equals(), causando notificações desnecessárias quando o mesmo valor é emitido novamente. StateFlow é a escolha certa para estado; SharedFlow é para eventos.
Para testar SharedFlow, use Turbine — uma biblioteca Kotlin para testar Flow. Turbine permite verificar cada emissão individualmente com timeouts e verificação de conclusão. Exemplo: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Você também pode usar .toList() em runTest especificando o número de eventos esperados.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também