SharedFlow là một luồng phản ứng nóng từ thư viện Kotlin Coroutines, được tối ưu hóa cho các sự kiện một lần (one-shot events) không nên lặp lại khi xoay màn hình hoặc tạo lại người đăng ký. Chúng tôi chỉ ra SharedFlow khác StateFlow như thế nào: không giống StateFlow, SharedFlow không lưu trữ giá trị cuối cùng cho người đăng ký mới và hỗ trợ cấu hình replay, extraBufferCapacity và onBufferOverflow. Theo Google (Android Developers, 2025), SharedFlow là giải pháp được khuyến nghị cho các lệnh điều hướng, thông báo Snackbar và các sự kiện khác cần được xử lý đúng một lần.
Những Điểm Chính
SharedFlow là một luồng nóng (hot flow) từ thư viện kotlinx.coroutines.flow, không giống StateFlow, không bị ràng buộc với một trạng thái duy nhất và có thể phát ra số lượng sự kiện tùy ý cho những người đăng ký tùy ý. SharedFlow là kiểu cơ sở cho StateFlow — StateFlow thực sự được triển khai thông qua SharedFlow với replay = 1.
Tính năng chính của SharedFlow là nó không phải lưu trữ giá trị cuối cùng. Theo mặc định (replay = 0), người đăng ký mới không nhận được gì cho đến khi một sự kiện mới được gửi. Điều này làm cho SharedFlow trở nên lý tưởng cho các tình huống mà một sự kiện cần được xử lý đúng một lần: điều hướng, Snackbar, thông báo hệ thống, kết quả quét mã QR.
SharedFlow đã được ổn định trong kotlinx.coroutines 1.4.0 (tháng 11 năm 2020) cùng với StateFlow. Theo tài liệu Kotlin Coroutines (2025), SharedFlow sử dụng khóa chi tiết để đồng bộ hóa người đăng ký và cung cấp khả năng mở rộng tuyến tính lên đến hơn 1000 người đăng ký đồng thời mà không suy giảm hiệu suất, được xác nhận bởi các thử nghiệm của JetBrains.
Việc lựa chọn giữa SharedFlow và StateFlow phụ thuộc vào ngữ nghĩa của dữ liệu được truyền: trạng thái (StateFlow) hay sự kiện (SharedFlow). Dưới đây là các tiêu chí rõ ràng với ví dụ.
| Tiêu chí | SharedFlow | StateFlow |
|---|---|---|
| Ngữ nghĩa | Sự kiện một lần (điều hướng, toast, cảnh báo) | Trạng thái UI (danh sách, tải, lỗi) |
| Giá trị ban đầu | Không yêu cầu | Bắt buộc |
| Phát lại khi đăng ký | Chỉ khi replay > 0 | Luôn là giá trị cuối cùng |
| Hợp nhất | Không — sự kiện không bị mất (nếu bộ đệm không đầy) | Có — chỉ lưu trữ giá trị mới nhất |
| Đệm | Có thể cấu hình qua replay + extraBufferCapacity | Chỉ 1 (replay=1 cố định) |
| Sử dụng | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
Quy tắc đơn giản nhất: nếu dữ liệu nên được hiển thị khi xoay màn hình — đó là trạng thái (StateFlow). Nếu khi xoay màn hình sự kiện không nên lặp lại — đó là sự kiện một lần (SharedFlow). Ví dụ: một toast với thông báo lỗi là SharedFlow: khi xoay, toast không nên xuất hiện lại. Một danh sách sản phẩm là StateFlow: khi xoay, danh sách sẽ vẫn ở trên màn hình.
Tại IT Sectr, chúng tôi sử dụng SharedFlow cho: lệnh điều hướng (chuyển màn hình, mở deep link), sự kiện UI (Snackbar, AlertDialog), thông báo hệ thống (cập nhật dữ liệu nền, kết quả thanh toán), sự kiện phân tích (ghi log, theo dõi).
MutableSharedFlow là phiên bản có thể thay đổi của SharedFlow với các phương thức emit() (suspend) và tryEmit() (không suspend) để gửi sự kiện. emit() tạm dừng nếu bộ đệm đầy và onBufferOverflow = SUSPEND. tryEmit() trả về Boolean cho biết sự kiện đã được thêm vào bộ đệm thành công hay chưa.
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
}
Các tham số của hàm tạo rất quan trọng: replay = 0 đảm bảo sự kiện không lặp lại cho người đăng ký mới; extraBufferCapacity = 10 cung cấp bộ đệm cho việc phát sự kiện nhanh trước khi UI đăng ký; DROP_OLDEST là chiến lược tràn: các sự kiện cũ bị loại bỏ, các sự kiện mới được giữ lại. Theo Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow với extraBufferCapacity = 64 xử lý hơn 100.000 sự kiện mỗi giây mà không mất mát.
Mẫu Event (hoặc UiEvent) là cách Google khuyến nghị để truyền các sự kiện một lần từ ViewModel đến View. Không giống như trạng thái (StateFlow), một sự kiện nên được xử lý đúng một lần và khi xoay màn hình, nó không nên lặp lại. SharedFlow với replay = 0 là lý tưởng cho nhiệm vụ này.
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 ?: "Lỗi bố cục"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
Trong View (Activity/Fragment): việc đăng ký sự kiện nên được thực hiện trong lifecycleScope với repeatOnLifecycle(STATE.STARTED). Mỗi khi vào STARTED, đăng ký được tạo lại, nhưng sự kiện không lặp lại vì SharedFlow với replay=0 đã giải phóng nó. Điều này đảm bảo rằng việc điều hướng đến màn hình theo dõi đơn hàng chỉ xảy ra một lần, không phải mỗi lần xoay.
Hàm tạo của MutableSharedFlow chấp nhận ba tham số xác định hành vi của bộ đệm. Cấu hình sai có thể dẫn đến mất sự kiện hoặc chặn emit().
| Tham số | Loại | Mặc định | Mô tả |
|---|---|---|---|
| replay | Int | 0 | Số lượng sự kiện gần đây được phát lại cho người đăng ký mới. 0 = không phát lại, 1 = như StateFlow |
| extraBufferCapacity | Int | 0 | Bộ đệm bổ sung vượt quá replay. Các sự kiện được lưu trữ trong bộ đệm vòng. 64 là giới hạn được khuyến nghị cho hầu hết các tình huống |
| onBufferOverflow | BufferOverflow | SUSPEND | Chiến lược khi bộ đệm đầy: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Cấu hình cho các tình huống khác nhau:
// 1. Sự kiện UI một lần (điều hướng, toast)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Luồng phát lại để đồng bộ trạng thái (như StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Phát sự kiện tần số cao (phân tích, nhật ký)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Quan trọng: extraBufferCapacity + replay = tổng kích thước bộ đệm. Nếu emit() được gọi nhanh hơn người đăng ký xử lý sự kiện, bộ đệm sẽ đầy và onBufferOverflow được kích hoạt. Đối với các sự kiện UI, DROP_OLDEST là một chiến lược an toàn: các sự kiện cũ (điều hướng không còn liên quan) bị loại bỏ để nhường chỗ cho các sự kiện mới. Đối với các giao dịch tài chính, hãy sử dụng SUSPEND — điều này đảm bảo không có sự kiện nào bị mất với cái giá phải chặn người gửi.
Các lệnh điều hướng là một trường hợp sử dụng cổ điển cho SharedFlow. Fragment đăng ký các sự kiện và thực hiện điều hướng. Khi xoay màn hình, lệnh không lặp lại.
// 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
}
// Trong 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()
}
}
}
}
Một tình huống phức tạp: SharedFlow cho thông báo sự kiện nền kết hợp với StateFlow cho 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("Thông báo mới: ${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("Tất cả thông báo đã được đánh dấu là đã đọc")
}
}
}
Trong ví dụ này: StateFlow lưu trữ danh sách thông báo (trạng thái — được giữ lại khi xoay), SharedFlow phát ra thông báo toast (sự kiện một lần — không lặp lại khi xoay). Sự kết hợp của hai loại Flow là mẫu được Google khuyến nghị cho ViewModel từ năm 2022.
Các Câu Hỏi Thường Gặp
Có, nếu bộ đệm đầy và onBufferOverflow = DROP_OLDEST hoặc DROP_LATEST. SharedFlow không đảm bảo phân phối mọi sự kiện — nó không phải là hàng đợi tin nhắn (như Channel). Nếu bạn cần phân phối đảm bảo tất cả các sự kiện, hãy sử dụng Channel với bộ đệm không giới hạn (UNLIMITED) hoặc BroadcastChannel (không được dùng nữa). Đối với các sự kiện UI, việc mất các sự kiện cũ (ví dụ: điều hướng cũ) là hành vi mong đợi, không phải lỗi.
Channel là hàng đợi FIFO, nơi mỗi sự kiện được phân phối đến đúng một người đăng ký (điểm-đến-điểm). SharedFlow là phát sóng: mỗi sự kiện được phân phối đến TẤT CẢ người đăng ký đang hoạt động. SharedFlow gần với BroadcastChannel (đã không được dùng nữa) và phù hợp cho các tình huống một-nhiều. Channel dành cho một-một (nhóm luồng, đường ống). Theo khuyến nghị của JetBrains, SharedFlow là sự thay thế cho BroadcastChannel trong tất cả các dự án mới.
SharedFlow đã an toàn với luồng — emit() và collect() được đồng bộ hóa đúng cách. Nhiều luồng có thể gọi emit() mà không cần khóa và tất cả người đăng ký đang hoạt động đều nhận được sự kiện theo đúng thứ tự. tryEmit() không chặn — nó trả về false nếu bộ đệm đầy. Đối với các hệ thống tải cao, hãy sử dụng tryEmit() với DROP_OLDEST — điều này ngăn chặn việc chặn luồng.
SharedFlow không có replay=1 không lưu trữ giá trị cuối cùng — khi xoay màn hình, người đăng ký mới sẽ không nhận được trạng thái hiện tại và UI sẽ trống. Với replay=1, SharedFlow hoạt động giống StateFlow nhưng mất đi tối ưu hóa so sánh thông qua equals(), gây ra thông báo không cần thiết khi cùng một giá trị được phát ra lại. StateFlow là lựa chọn đúng cho trạng thái; SharedFlow dành cho sự kiện.
Để kiểm tra SharedFlow, hãy sử dụng Turbine — một thư viện Kotlin để kiểm tra Flow. Turbine cho phép kiểm tra từng lần phát riêng lẻ với thời gian chờ và xác minh hoàn thành. Ví dụ: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Bạn cũng có thể sử dụng .toList() trong runTest với việc chỉ định số lượng sự kiện dự kiến.
Tổng Kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm