SharedFlow: nó là gì, SharedFlow vs StateFlow trong Android

Tác giả: IT Sectr Đã đăng: 2026-02-20 Thời gian đọc: 9 phút

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 — luồng nóng cho các sự kiện một lần: người đăng ký mới không nhận được giá trị trước đó nếu không có cấu hình replay.
  • MutableSharedFlow — phiên bản có thể thay đổi với các phương thức emit() và tryEmit() để gửi sự kiện.
  • SharedFlow vs StateFlow: SharedFlow không hợp nhất giá trị (có thể đệm nhiều giá trị), không yêu cầu giá trị ban đầu, phù hợp cho các sự kiện một lần.
  • replay — số lượng sự kiện gần đây được phát lại cho người đăng ký mới (mặc định 0).
  • extraBufferCapacity — bộ đệm bổ sung cho các sự kiện vượt quá replay, ngăn chặn việc chặn emit().

SharedFlow trong Kotlin là gì?

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.

SharedFlow vs StateFlow: khi nào sử dụng cái gì

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íSharedFlowStateFlow
Ngữ nghĩaSự 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 đầuKhông yêu cầuBắt buộc
Phát lại khi đăng kýChỉ khi replay > 0Luôn là giá trị cuối cùng
Hợp nhấtKhô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
ĐệmCó thể cấu hình qua replay + extraBufferCapacityChỉ 1 (replay=1 cố định)
Sử dụngnavigationEvent, showSnackbar, openDialogitems, 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: emit, tryEmit và bộ đệm

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.

kotlin
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.

SharedFlow cho sự kiện một lần: mẫu Event

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.

kotlin
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.

Tham số SharedFlow: replay, extraBufferCapacity, onBufferOverflow

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ạiMặc địnhMô tả
replayInt0Số 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
extraBufferCapacityInt0Bộ đệ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
onBufferOverflowBufferOverflowSUSPENDChiến lược khi bộ đệm đầy: SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// 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.

Ví dụ mã: SharedFlow trong Kotlin

Ví dụ 1: SharedFlow cho điều hướng với Jetpack Navigation

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.

kotlin
// 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()
            }
        }
    }
}

Ví dụ 2: SharedFlow với Room và các toán tử Flow

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.

kotlin
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

SharedFlow có thể mất một sự kiện không?

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.

SharedFlow khác Channel như thế nào?

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.

Làm thế nào để SharedFlow an toàn với luồng?

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.

Tại sao SharedFlow không được sử dụng cho trạng thái?

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.

Làm thế nào để kiểm tra SharedFlow?

Để 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

  • SharedFlow — một luồng phản ứng nóng cho các sự kiện một lần không gắn với trạng thái cuối cùng.
  • SharedFlow vs StateFlow: SharedFlow cho sự kiện (điều hướng, toast, cảnh báo), StateFlow cho trạng thái (danh sách, tải, lỗi).
  • MutableSharedFlow với replay=0, extraBufferCapacity=5, DROP_OLDEST là cấu hình tiêu chuẩn cho các sự kiện UI.
  • emit() — hàm suspend để phát chặn; tryEmit() — không suspend với kết quả Boolean.
  • Mẫu UiEvent với sealed class là cách Google khuyến nghị để truyền các sự kiện một lần từ ViewModel đến View.
  • SharedFlow đảm bảo phân phối đến mỗi người đăng ký, nhưng không đảm bảo phân phối mỗi sự kiện khi bộ đệm tràn.
  • Sự kết hợp SharedFlow + StateFlow trong một ViewModel duy nhất là mẫu tối ưu cho kiến trúc tách biệt trạng thái và sự kiện.

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.

Thảo luận dự án

Đọc thêm