SharedFlow คืออะไร SharedFlow vs StateFlow ใน Android

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-02-20 เวลาอ่าน: 9 นาที

SharedFlow คือสตรีมแบบ hot flow จากไลบรารี Kotlin Coroutines ที่ถูกปรับให้เหมาะสมสำหรับอีเวนต์แบบครั้งเดียว (one-shot events) ที่ไม่ควรเกิดขึ้นซ้ำเมื่อหมุนหน้าจอหรือเมื่อมีการสร้าง subscriber ใหม่ เราจะแสดงให้เห็นว่า SharedFlow แตกต่างจาก StateFlow อย่างไร: ไม่เหมือน StateFlow ตรงที่ SharedFlow ไม่เก็บค่าล่าสุดสำหรับ subscriber ใหม่และรองรับการกำหนดค่า replay, extraBufferCapacity และ onBufferOverflow ตามข้อมูลของ Google (Android Developers, 2025) SharedFlow เป็นโซลูชันที่แนะนำสำหรับคำสั่งนำทาง ข้อความ Snackbar และอีเวนต์อื่น ๆ ที่ควรถูกประมวลผลเพียงครั้งเดียว

สาระสำคัญ

  • SharedFlow — hot flow สำหรับอีเวนต์แบบครั้งเดียว: subscriber ใหม่จะไม่ได้รับค่าก่อนหน้าโดยไม่มีการตั้งค่า replay
  • MutableSharedFlow — เวอร์ชันที่แก้ไขได้พร้อมเมธอด emit() และ tryEmit() สำหรับส่งอีเวนต์
  • SharedFlow vs StateFlow: SharedFlow ไม่รวมค่าต่าง ๆ (สามารถบัฟเฟอร์หลายค่าได้) ไม่ต้องการค่าเริ่มต้น เหมาะสำหรับอีเวนต์แบบครั้งเดียว
  • replay — จำนวนอีเวนต์ล่าสุดที่จะเล่นซ้ำให้กับ subscriber ใหม่ (ค่าเริ่มต้นคือ 0)
  • extraBufferCapacity — บัฟเฟอร์เพิ่มเติมนอกเหนือจาก replay เพื่อป้องกันการบล็อกของ emit()

SharedFlow ใน Kotlin คืออะไร?

SharedFlow คือ hot flow จากไลบรารี kotlinx.coroutines.flow ซึ่งแตกต่างจาก StateFlow ตรงที่ไม่ผูกติดกับสถานะเดียวและสามารถ emit อีเวนต์จำนวนเท่าใดก็ได้ให้กับ subscriber จำนวนเท่าใดก็ได้ SharedFlow เป็นชนิดพื้นฐานของ StateFlow — StateFlow ถูก implement ผ่าน SharedFlow โดยมี replay = 1

คุณลักษณะสำคัญของ SharedFlow คือมันไม่จำเป็นต้องเก็บค่าล่าสุด โดยค่าเริ่มต้น (replay = 0) subscriber ใหม่จะไม่ได้รับอะไรเลยจนกว่าจะมีการส่งอีเวนต์ใหม่ ซึ่งทำให้ SharedFlow เหมาะอย่างยิ่งสำหรับสถานการณ์ที่อีเวนต์ควรถูกประมวลผลเพียงครั้งเดียว: การนำทาง Snackbar การแจ้งเตือนระบบ ผลการสแกน QR โค้ด

SharedFlow ถูกทำให้เสถียรใน kotlinx.coroutines 1.4.0 (พฤศจิกายน 2020) พร้อมกับ StateFlow ตามเอกสารประกอบ Kotlin Coroutines (2025) SharedFlow ใช้การล็อกแบบละเอียดเพื่อซิงโครไนซ์ subscriber และให้ ความสามารถในการปรับขนาดเชิงเส้น สูงถึง subscriber พร้อมกัน 1000+ รายโดยไม่ลดประสิทธิภาพ ซึ่งได้รับการยืนยันโดยการทดสอบของ JetBrains

SharedFlow vs StateFlow: เมื่อใดควรใช้อะไร

การเลือกระหว่าง SharedFlow และ StateFlow ขึ้นอยู่กับความหมายของข้อมูลที่ส่ง: สถานะ (StateFlow) หรืออีเวนต์ (SharedFlow) ด้านล่างคือเกณฑ์ที่ชัดเจนพร้อมตัวอย่าง

เกณฑ์SharedFlowStateFlow
ความหมายอีเวนต์แบบครั้งเดียว (การนำทาง, toast, alert)สถานะ UI (รายการ, การโหลด, ข้อผิดพลาด)
ค่าเริ่มต้นไม่จำเป็นจำเป็น
การทำซ้ำเมื่อสมัครสมาชิกเฉพาะเมื่อ replay > 0ค่าล่าสุดเสมอ
การรวมค่าไม่ — อีเวนต์จะไม่สูญหาย (ถ้าบัฟเฟอร์ไม่ล้น)ใช่ — เก็บเฉพาะค่าล่าสุด
การบัฟเฟอร์ปรับแต่งได้ผ่าน replay + extraBufferCapacityเพียง 1 (replay=1 คงที่)
การใช้งานnavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

กฎที่ง่ายที่สุด: ถ้าข้อมูลควรแสดงเมื่อหมุนหน้าจอ — นี่คือสถานะ (StateFlow) ถ้าเมื่อหมุนหน้าจออีเวนต์ไม่ควรเกิดขึ้นซ้ำ — นี่คืออีเวนต์แบบครั้งเดียว (SharedFlow) ตัวอย่างเช่น «toast พร้อมข้อความแสดงข้อผิดพลาด» — SharedFlow: เมื่อหมุนหน้าจอ toast ไม่ควรแสดงอีกครั้ง «รายการสินค้า» — StateFlow: เมื่อหมุนหน้าจอรายการควรยังคงอยู่บนหน้าจอ

ใน IT Sectr เราใช้ SharedFlow สำหรับ: คำสั่งนำทาง (การเปลี่ยนหน้าจอ, การเปิด deep link), อีเวนต์ UI (Snackbar, AlertDialog), การแจ้งเตือนระบบ (การอัปเดตข้อมูลในพื้นหลัง, ผลลัพธ์การชำระเงิน), อีเวนต์การวิเคราะห์ (การบันทึก, การติดตาม)

MutableSharedFlow: emit, tryEmit และการบัฟเฟอร์

MutableSharedFlow คือเวอร์ชันที่แก้ไขได้ของ SharedFlow พร้อมเมธอด emit() (suspend) และ tryEmit() (non-suspend) สำหรับส่งอีเวนต์ emit() จะถูกระงับถ้าบัฟเฟอร์เต็มและ onBufferOverflow = SUSPEND tryEmit() คืนค่า Boolean — ว่าอีเวนต์ถูกเพิ่มลงในบัฟเฟอร์สำเร็จหรือไม่

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
}

พารามิเตอร์ของคอนสตรัคเตอร์มีความสำคัญ: replay = 0 รับประกันว่าอีเวนต์จะไม่เกิดขึ้นซ้ำสำหรับ subscriber ใหม่ extraBufferCapacity = 10 — บัฟเฟอร์ในกรณีที่มีการส่งอีเวนต์อย่างรวดเร็วก่อนที่ UI จะสมัครสมาชิก DROP_OLDEST — กลยุทธ์เมื่อบัฟเฟอร์ล้น: อีเวนต์เก่าจะถูกทิ้ง อีเวนต์ใหม่จะถูกเก็บไว้ ตามข้อมูลประสิทธิภาพ Kotlin Coroutines (JetBrains, 2024) SharedFlow ที่มี extraBufferCapacity = 64 สามารถประมวลผลมากกว่า 100,000 อีเวนต์ต่อวินาที โดยไม่สูญเสีย

SharedFlow สำหรับอีเวนต์แบบครั้งเดียว: รูปแบบ Event

รูปแบบ Event (หรือ UiEvent) — วิธีที่ Google แนะนำสำหรับการส่งอีเวนต์แบบครั้งเดียวจาก ViewModel ไปยัง View แตกต่างจากสถานะ (StateFlow) ตรงที่อีเวนต์ควรถูกประมวลผลเพียงครั้งเดียว และเมื่อหมุนหน้าจออีเวนต์ไม่ควรเกิดขึ้นซ้ำ SharedFlow ที่มี replay = 0 เหมาะอย่างยิ่งสำหรับงานนี้

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 ?: "ข้อผิดพลาดในการสั่งซื้อ"))
            }
        }
    }
}

sealed interface CheckoutEvent {
    data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
    data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}

ใน View (Activity/Fragment): การสมัครสมาชิกอีเวนต์ควรทำใน lifecycleScope ด้วย repeatOnLifecycle(STATE.STARTED) ทุกครั้งที่เข้าสู่ STARTED การสมัครสมาชิกจะถูกสร้างขึ้นใหม่ แต่อีเวนต์จะไม่เกิดขึ้นซ้ำเพราะ SharedFlow ที่มี replay=0 ได้ปล่อยมันไปแล้ว ซึ่งรับประกันว่าการนำทางไปยังหน้าจอติดตามคำสั่งซื้อจะเกิดขึ้นเพียงครั้งเดียว ไม่ใช่ทุกครั้งที่หมุนหน้าจอ

พารามิเตอร์ของ SharedFlow: replay, extraBufferCapacity, onBufferOverflow

คอนสตรัคเตอร์ของ MutableSharedFlow รับพารามิเตอร์สามตัวที่กำหนดพฤติกรรมของบัฟเฟอร์ การตั้งค่าที่ไม่ถูกต้องอาจทำให้สูญเสียอีเวนต์หรือการบล็อกของ emit()

พารามิเตอร์ชนิดค่าเริ่มต้นคำอธิบาย
replayInt0จำนวนอีเวนต์ล่าสุดที่จะเล่นซ้ำให้ subscriber ใหม่ 0 = ไม่เล่นซ้ำ, 1 = เหมือน StateFlow
extraBufferCapacityInt0บัฟเฟอร์เพิ่มเติมนอกเหนือจาก replay อีเวนต์ถูกเก็บในบัฟเฟอร์แบบวงกลม 64 — ขีดจำกัดที่แนะนำสำหรับสถานการณ์ส่วนใหญ่
onBufferOverflowBufferOverflowSUSPENDกลยุทธ์เมื่อบัฟเฟอร์เต็ม: SUSPEND, DROP_OLDEST, DROP_LATEST
// การกำหนดค่าสำหรับสถานการณ์ต่าง ๆ:

// 1. อีเวนต์ UI แบบครั้งเดียว (การนำทาง, toast)
val uiEvents = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 5,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// 2. สตรีม Replay สำหรับการซิงค์สถานะ (เหมือน StateFlow)
val stateLike = MutableSharedFlow<AppState>(
    replay = 1,
    extraBufferCapacity = 0
)

// 3. การส่งอีเวนต์ความถี่สูง (การวิเคราะห์, บันทึก)
val analytics = MutableSharedFlow<AnalyticsEvent>(
    replay = 0,
    extraBufferCapacity = 100,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

สำคัญ: extraBufferCapacity + replay = ขนาดบัฟเฟอร์ทั้งหมด ถ้า emit() ถูกเรียกเร็วกว่าที่ subscriber จะประมวลผลอีเวนต์ บัฟเฟอร์จะเต็มและ onBufferOverflow จะทำงาน สำหรับอีเวนต์ UI DROP_OLDEST เป็นกลยุทธ์ที่ปลอดภัย: อีเวนต์เก่า (การนำทางที่ไม่เกี่ยวข้องอีกต่อไป) จะถูกทิ้งเพื่อให้อีเวนต์ใหม่ สำหรับธุรกรรมทางการเงิน ให้ใช้ SUSPEND — ซึ่งรับประกันว่าจะไม่มีอีเวนต์สูญหายแม้ต้องแลกกับการบล็อกผู้ส่ง

ตัวอย่างโค้ด: SharedFlow ใน Kotlin

ตัวอย่างที่ 1: SharedFlow สำหรับการนำทางด้วย Jetpack Navigation

คำสั่งนำทางเป็นกรณีการใช้งานคลาสสิกสำหรับ SharedFlow Fragment สมัครสมาชิกอีเวนต์และดำเนินการนำทาง เมื่อหมุนหน้าจอ คำสั่งจะไม่เกิดขึ้นซ้ำ

// 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
}

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

ตัวอย่างที่ 2: SharedFlow กับ Room และ Flow Operators

สถานการณ์ที่ซับซ้อน: SharedFlow สำหรับการแจ้งเตือนอีเวนต์พื้นหลังรวมกับ StateFlow สำหรับ 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("การแจ้งเตือนใหม่: ${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("การแจ้งเตือนทั้งหมดถูกทำเครื่องหมายว่าอ่านแล้ว")
        }
    }
}

ในตัวอย่างนี้: StateFlow เก็บรายการการแจ้งเตือน (สถานะ — ยังคงอยู่เมื่อหมุนหน้าจอ), SharedFlow ส่งข้อความ toast (อีเวนต์แบบครั้งเดียว — ไม่เกิดขึ้นซ้ำเมื่อหมุนหน้าจอ) การรวม Flow ทั้งสองชนิดเป็นรูปแบบที่ Google แนะนำสำหรับ ViewModel ตั้งแต่ปี 2022

คำถามที่พบบ่อย

SharedFlow สามารถสูญเสียอีเวนต์ได้หรือไม่?

ได้ ถ้า บัฟเฟอร์ล้น และ onBufferOverflow = DROP_OLDEST หรือ DROP_LATEST SharedFlow ไม่รับประกันการส่งทุกอีเวนต์ — มันไม่ใช่คิวข้อความ (เหมือน Channel) ถ้าต้องการการส่งทุกอีเวนต์อย่างแน่นอน ให้ใช้ Channel ที่มีบัฟเฟอร์ไม่จำกัด (UNLIMITED) หรือ BroadcastChannel (เลิกใช้แล้ว) สำหรับอีเวนต์ UI การสูญเสียอีเวนต์ที่ล้าสมัย (เช่น การนำทางเก่า) เป็นพฤติกรรมที่คาดหวัง ไม่ใช่บั๊ก

SharedFlow แตกต่างจาก Channel อย่างไร?

Channel คือคิว FIFO ที่แต่ละอีเวนต์ถูกส่งไปยัง subscriber เพียงรายเดียว (จุดต่อจุด) SharedFlow คือการแพร่ภาพ: แต่ละอีเวนต์ถูกส่งไปยัง subscriber ที่ทำงานอยู่ทั้งหมด SharedFlow ใกล้เคียงกับ BroadcastChannel (ซึ่งเลิกใช้แล้ว) และเหมาะสำหรับสถานการณ์ «หนึ่งต่อหลาย» Channel สำหรับ «หนึ่งต่อหนึ่ง» (พูลเธรด, pipeline) ตามคำแนะนำของ JetBrains SharedFlow คือสิ่งทดแทน BroadcastChannel สำหรับโปรเจกต์ใหม่ทั้งหมด

จะทำให้ SharedFlow ปลอดภัยต่อเธรดได้อย่างไร?

SharedFlow ปลอดภัยต่อเธรดอยู่แล้ว — emit() และ collect() ถูกซิงโครไนซ์อย่างถูกต้อง หลายเธรดสามารถเรียก emit() โดยไม่ต้องล็อก และ subscriber ที่ทำงานอยู่ทั้งหมดจะได้รับอีเวนต์ตามลำดับที่ถูกต้อง tryEmit() ไม่บล็อก — คืนค่า false ถ้าบัฟเฟอร์เต็ม สำหรับระบบที่มีโหลดสูง ให้ใช้ tryEmit() กับ DROP_OLDEST — ซึ่งป้องกันการบล็อกของเธรด

ทำไม SharedFlow ไม่ถูกใช้สำหรับสถานะ?

SharedFlow ที่ไม่มี replay=1 จะไม่เก็บค่าล่าสุด — เมื่อหมุนหน้าจอ subscriber ใหม่จะไม่ได้รับสถานะปัจจุบัน UI จะว่างเปล่า เมื่อมี replay=1 SharedFlow จะทำงานเหมือน StateFlow แต่สูญเสียการเพิ่มประสิทธิภาพการเปรียบเทียบผ่าน equals() ซึ่งทำให้เกิดการแจ้งเตือนที่ไม่จำเป็นเมื่อส่งค่าเดียวกันซ้ำ StateFlow เป็นตัวเลือกที่ถูกต้องสำหรับสถานะ; SharedFlow สำหรับอีเวนต์

จะทดสอบ SharedFlow ได้อย่างไร?

สำหรับการทดสอบ SharedFlow ให้ใช้ Turbine — ไลบรารี Kotlin สำหรับทดสอบ Flow Turbine ช่วยให้คุณตรวจสอบแต่ละ emission แยกกันพร้อมการตั้งเวลาและการตรวจสอบความสมบูรณ์ ตัวอย่าง: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) } นอกจากนี้ยังสามารถใช้ .toList() ใน runTest โดยระบุจำนวนอีเวนต์ที่คาดหวัง

สรุป

  • SharedFlow — hot flow สำหรับอีเวนต์แบบครั้งเดียวที่ไม่ผูกติดกับสถานะล่าสุด
  • SharedFlow vs StateFlow: SharedFlow — อีเวนต์ (การนำทาง, toast, alert), StateFlow — สถานะ (รายการ, การโหลด, ข้อผิดพลาด)
  • MutableSharedFlow ที่มี replay=0, extraBufferCapacity=5, DROP_OLDEST — การกำหนดค่ามาตรฐานสำหรับอีเวนต์ UI
  • emit() — suspend function สำหรับการส่งแบบบล็อก; tryEmit() — non-suspend ที่ให้ผลลัพธ์เป็น Boolean
  • รูปแบบ UiEvent กับ sealed class — วิธีที่แนะนำสำหรับการส่งอีเวนต์แบบครั้งเดียวจาก ViewModel ไปยัง View
  • SharedFlow รับประกันการส่งถึงแต่ละ subscriber แต่ไม่รับประกัน การส่งทุกอีเวนต์ เมื่อบัฟเฟอร์ล้น
  • การรวม SharedFlow + StateFlow ใน ViewModel เดียวกัน — รูปแบบที่เหมาะสมที่สุดสำหรับสถาปัตยกรรมที่แยกสถานะและอีเวนต์

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม