Kotlin Flow vs StateFlow vs SharedFlow: คำถามสัมภาษณ์ Android ปี 2026

คำถาม Kotlin Flow vs StateFlow vs SharedFlow ที่ผู้สัมภาษณ์ Android ถามในปี 2026 พร้อมคำตอบชัดเจน ตารางเปรียบเทียบ และโค้ดที่พร้อมใช้งานจริง

การเปรียบเทียบ Kotlin Flow vs StateFlow vs SharedFlow สำหรับการสัมภาษณ์ Android

Kotlin Flow vs StateFlow vs SharedFlow เป็นหนึ่งในหัวข้อสัมภาษณ์ Android ที่พบบ่อยที่สุดในปี 2026 และการสับสนระหว่างทั้งสามตัวนี้คือทางลัดสู่การตกรอบ coroutines StateFlow และ SharedFlow ต่างก็เป็น hot stream ที่สร้างขึ้นบน Flow แต่มีไว้เพื่อแก้ปัญหาคนละอย่าง คือการเก็บสถานะ (state) กับการกระจายอีเวนต์ (event) คำถามด้านล่างนี้คือคำถามที่ผู้สัมภาษณ์ Android ถามจริง พร้อมคำตอบที่แม่นยำและโค้ดที่พร้อมใช้งานจริง

คำตอบภายใน 20 วินาที

Flow เป็นแบบ cold และรัน producer หนึ่งครั้งต่อ collector หนึ่งตัว StateFlow เป็น hot stream แบบ conflated ที่เก็บค่าปัจจุบันเพียงค่าเดียวไว้เสมอ เหมาะกับสถานะของ UI ส่วน SharedFlow เป็น hot stream ที่ไม่จำเป็นต้องมีค่าเริ่มต้น เหมาะกับอีเวนต์ที่เกิดขึ้นครั้งเดียว เช่น การนำทาง (navigation) หรือ snackbar

Kotlin Flow vs StateFlow vs SharedFlow: ความแตกต่างหลัก

StateFlow และ SharedFlow เป็นรูปแบบเฉพาะทาง (specialization) ของ SharedFlow ซึ่งตัวมันเองก็เป็น hot Flow สิ่งที่ผู้สัมภาษณ์มักเจาะลึกคือความต่างระหว่าง cold กับ hot ว่ามีการเก็บค่าปัจจุบันไว้หรือไม่ และแต่ละตัวจัดการกับการ emit ค่าซ้ำอย่างไร ภาพจำง่ายๆ คือ Flow เปรียบเหมือนสูตรอาหาร StateFlow เปรียบเหมือนค่าที่เปลี่ยนแปลงได้เพียงค่าเดียว และ SharedFlow เปรียบเหมือน event bus

| คุณสมบัติ | Flow (cold) | StateFlow | SharedFlow | |---|---|---|---| | อุณหภูมิ | Cold | Hot | Hot | | ค่าเริ่มต้น | ไม่มี | จำเป็นต้องมี | มีหรือไม่ก็ได้ (ผ่าน replay) | | เก็บค่าปัจจุบัน | ไม่ | ใช่ ผ่าน .value | ไม่ | | emit ค่าซ้ำ | ได้ | ไม่ (conflated + ตัดค่าซ้ำ) | ตั้งค่าได้ | | collector เพิ่มเติม | สร้าง stream ใหม่ทุกครั้ง | ใช้ร่วมกัน | ใช้ร่วมกัน | | เหมาะสำหรับ | ไปป์ไลน์ข้อมูลแบบ async | สถานะ UI | อีเวนต์ที่เกิดครั้งเดียว |

เอกสารทางการของ Kotlin coroutines ถือว่าพฤติกรรม cold-by-default นี้คือคุณสมบัติหลักที่นิยาม Flow ดังนั้นคำตอบที่ดีจึงควรเริ่มจากจุดนี้

ทำไม Kotlin Flow ถึงเป็น cold โดยค่าเริ่มต้น?

cold flow จะไม่ทำงานใดๆ จนกว่าจะมีการเรียก collect() และมันจะรันบล็อก producer ใหม่ทุกครั้งสำหรับ collector แต่ละตัว collector สองตัวบน cold flow เดียวกันจะทำให้เกิดการเรียก network สองครั้งที่แยกจากกัน นี่คือแนวคิดที่ถูกทดสอบบ่อยที่สุด ตัวอย่างสั้นๆ จึงช่วยยึดคำตอบให้ชัดเจน

SearchRepository.ktkotlin
fun searchResults(query: String): Flow<List<Result>> = flow {
    // This block runs fresh for every collector.
    // Nothing executes until a collector calls collect().
    val results = api.search(query) // suspending network call
    emit(results)                   // pushed downstream to the collector
}

เนื่องจาก producer เริ่มทำงานใหม่สำหรับแต่ละ collector cold flow จึงเป็นตัวเลือกเริ่มต้นที่เหมาะสมสำหรับไปป์ไลน์ข้อมูลที่ควรทำงานตามความต้องการ (on demand) มันจะกลายเป็นปัญหาก็ต่อเมื่อต้องแชร์ค่าเดียวกันไปทั้งหน้าจอ ซึ่งเป็นจุดที่ StateFlow และ SharedFlow เข้ามามีบทบาทพอดี

คำถามสัมภาษณ์เรื่อง StateFlow: สถานะที่ emit เสมอ

StateFlow เป็น hot flow ที่มีค่าอยู่เสมอ และจะ replay ค่าล่าสุดนั้นให้กับ collector ใหม่ทุกตัว มันจำเป็นต้องมีค่าเริ่มต้น เปิดให้อ่านค่าแบบ synchronous ผ่าน .value และทำ conflate การ emit กล่าวคือการอัปเดตต่อเนื่องอย่างรวดเร็วอาจข้ามค่าระหว่างกลางไป และการกำหนดค่าเดิมซ้ำจะไม่ emit อะไรเลยเพราะมันตัดค่าซ้ำด้วยการเปรียบเทียบความเท่ากัน (equality) คุณสมบัตินี้ทำให้มันเป็นตัวแทนสมัยใหม่ของ LiveData ใน ViewModel

SearchViewModel.ktkotlin
class SearchViewModel(private val repo: SearchRepository) : ViewModel() {
    // MutableStateFlow demands an initial value, so the UI always has something to render.
    private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Idle)
    val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow()

    fun onQueryChanged(query: String) {
        _uiState.value = SearchUiState.Loading
        viewModelScope.launch {
            val results = repo.search(query)
            // update {} applies the change atomically, safe under concurrent callers.
            _uiState.update { SearchUiState.Success(results) }
        }
    }
}

คำถามต่อเนื่องที่พบบ่อยคือ ทำไมต้องเปิดเผย asStateFlow() แทนที่จะใช้ฟิลด์แบบ mutable โดยตรง? เพราะมันมอบมุมมองแบบอ่านอย่างเดียว (read-only) ให้กับ UI ทำให้สถานะเปลี่ยนแปลงได้ผ่าน ViewModel เท่านั้น ซึ่งช่วยรักษาการไหลของข้อมูลแบบทิศทางเดียว (unidirectional data flow) เอาไว้ คู่มือ StateFlow และ SharedFlow ของ Android เองก็แนะนำรูปแบบนี้อย่างชัดเจน

SharedFlow vs StateFlow: เลือกชนิดที่เหมาะกับอีเวนต์

คำถามหลุมพรางคลาสสิกคือ "StateFlow ส่งอีเวนต์ที่เกิดครั้งเดียวได้ไหม?" คำตอบตามตรงคือไม่ได้ อย่างน้อยก็ไม่ปลอดภัย เพราะ StateFlow ทำ conflate และตัดค่าซ้ำ อีเวนต์การนำทางจึงอาจถูกทิ้งไปเมื่อมีการอัปเดตอย่างรวดเร็ว และมันจะยิงซ้ำเมื่อเกิดการเปลี่ยน configuration เพราะ collector ใหม่จะ replay ค่าที่เก็บไว้ SharedFlow ที่ตั้ง replay = 0 แก้ปัญหาทั้งสองข้อได้ คือไม่มีการเก็บค่าใดไว้ และทุกการ emit จะไปถึงเฉพาะ collector ที่ทำงานอยู่ ณ ตอนที่ emit เท่านั้น

CheckoutViewModel.ktkotlin
class CheckoutViewModel : ViewModel() {
    // replay = 0: a late collector must NOT re-receive a past navigation event.
    private val _events = MutableSharedFlow<CheckoutEvent>(replay = 0)
    val events: SharedFlow<CheckoutEvent> = _events.asSharedFlow()

    fun onPaymentConfirmed() {
        viewModelScope.launch {
            // emit() suspends if the buffer is full; tryEmit() is the non-suspending variant.
            _events.emit(CheckoutEvent.NavigateToReceipt)
        }
    }
}

สำหรับคำถามด้านสถาปัตยกรรม เหตุผลเชิงลึกเบื้องหลังการแยกสถานะออกจากอีเวนต์ปรากฏอยู่ใน การเปรียบเทียบ MVVM vs MVI ซึ่ง MVI มองว่าอีเวนต์เป็น stream ที่ชัดเจน แทนที่จะเป็นสถานะแบบ mutable

การแปลง cold Flow เป็น hot StateFlow ด้วย stateIn

ผู้สัมภาษณ์มักถามวิธีเปลี่ยน cold flow จาก repository ให้กลายเป็นสถานะของ UI คำตอบคือ stateIn (สำหรับค่าเดียวที่ถูกเก็บไว้) หรือ shareIn (สำหรับการกระจายที่ไม่มีค่าปัจจุบัน) พารามิเตอร์ SharingStarted ควบคุมว่าเมื่อใดที่ upstream จะทำงาน และ WhileSubscribed(5_000) เป็นตัวเลือกมาตรฐาน เพราะมันทำให้ flow ยังทำงานอยู่ตลอดการหมุนจอ โดยไม่ทำให้เกิด leak เมื่อปิดหน้าจอ

ProfileViewModel.ktkotlin
val profile: StateFlow<Profile> = repo.profileStream()
    .stateIn(
        scope = viewModelScope,
        // Keep the upstream alive 5s after the last collector leaves, surviving rotation.
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = Profile.EMPTY
    )

ความต่างระหว่าง stateIn กับ shareIn ก็คือความต่างระหว่าง StateFlow กับ SharedFlow อย่างชัดเจน stateIn ต้องการ initialValue และเก็บสถานะไว้ ในขณะที่ shareIn รับค่าจำนวน replay และทำหน้าที่กระจาย การทบทวนโมเดล coroutines ในภาพกว้างใน คู่มือการเชี่ยวชาญ Kotlin coroutines นี้ ช่วยเชื่อมโยงตัวดำเนินการเหล่านี้เข้ากับ scope และการยกเลิก (cancellation)

พร้อมที่จะพิชิตการสัมภาษณ์ Android แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การ collect flow อย่างปลอดภัยตาม lifecycle ของ Android

คำถามระดับ senior คือจะ collect flow อย่างไรโดยไม่ทำให้เกิดงานที่รั่วไหลขณะที่หน้าจออยู่เบื้องหลัง (background) การ collect ภายใน launch แบบธรรมดาจะยังทำงานต่อไปแม้ UI จะหยุดแล้ว ทำให้สิ้นเปลือง CPU และเสี่ยงต่อการ crash repeatOnLifecycle(STARTED) จะยกเลิกการ collect เมื่อถึง STOP และเริ่มใหม่เมื่อถึง START

ProfileFragment.kt (View system)kotlin
viewLifecycleOwner.lifecycleScope.launch {
    // Collection restarts on STARTED and cancels on STOPPED: no wasted work in the background.
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state -> render(state) }
    }
}

ใน Jetpack Compose สิ่งที่เทียบเท่ากันคือ collectAsStateWithLifecycle() ซึ่งจะหยุด collect เมื่อแอปถูกย้ายไปเบื้องหลัง และกลับมาทำงานต่อเมื่อกลับเข้าแอป

ProfileScreen.kt (Jetpack Compose)kotlin
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    // collectAsStateWithLifecycle stops collecting when the app is backgrounded.
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileContent(state)
}

ความตระหนักในเรื่อง lifecycle นี้เองคือเหตุผลที่ StateFlow ควบคู่กับ collectAsStateWithLifecycle กลายเป็นรูปแบบการจัดการสถานะเริ่มต้นในแอป Compose สมัยใหม่ ประเด็นนี้มีอธิบายเพิ่มเติมใน คำถามสัมภาษณ์ Jetpack Compose

วิธีทดสอบการ emit ของ StateFlow และ SharedFlow

การทดสอบคือจุดที่แยกผู้สมัครระดับ senior ออกจากคนอื่น วิธีที่มือใหม่มักใช้คืออ่าน stateFlow.value เพียงครั้งเดียว แต่วิธีนี้พลาดสถานะระหว่างกลาง และไม่สามารถสังเกต SharedFlow ได้เลย คำตอบมาตรฐานคือการเอ่ยถึงไลบรารี Turbine ซึ่งจะ suspend จนกว่าการ emit แต่ละครั้งจะมาถึง และทำให้เทสต์ล้มเหลวหากค่าที่คาดหวังไม่เคยมาถึง

SearchViewModelTest.ktkotlin
@Test
fun `emits Loading then Success on query`() = runTest {
    val viewModel = SearchViewModel(fakeRepo)

    viewModel.uiState.test {
        assertEquals(SearchUiState.Idle, awaitItem())     // initial value
        viewModel.onQueryChanged("kotlin")
        assertEquals(SearchUiState.Loading, awaitItem())  // intermediate state
        assertEquals(SearchUiState.Success(fakeResults), awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

การใช้ Turbine ควบคู่กับ runTest และ test dispatcher ช่วยให้การ assert มีผลลัพธ์ที่แน่นอน (deterministic) และการพูดถึงความต่างระหว่าง StandardTestDispatcher กับ UnconfinedTestDispatcher แสดงถึงประสบการณ์การทดสอบ coroutine อย่างแท้จริง

คำถามต่อเนื่องเรื่อง Kotlin Flow ที่พบบ่อย

ผู้สัมภาษณ์มักปิดท้ายด้วยคำถามยิงเร็ว SharingStarted.WhileSubscribed(5_000) ทำอะไร? มันเริ่ม upstream เมื่อมีผู้ subscribe รายแรก และหยุดหลังจากผู้ subscribe รายสุดท้ายยกเลิกไป 5 วินาที ซึ่งนานพอที่จะรอดจากการหมุนจอ ทำไม StateFlow ถึงข้ามค่าที่ซ้ำกัน? เพราะมันเปรียบเทียบด้วย equals() การ emit ค่าที่เท่ากันจึงไม่มีผลอะไร นี่คือเหตุผลว่าทำไม data class จึงสำคัญต่อสถานะ SharedFlow ทำตัวเหมือน StateFlow ได้ไหม? การตั้ง replay = 1 ทำให้มันเก็บค่าล่าสุดไว้ แต่มันก็ยังไม่มี .value แบบ synchronous และไม่เคยตัดค่าซ้ำ อะไรควบคุม back-pressure บน SharedFlow? พารามิเตอร์ extraBufferCapacity และ onBufferOverflow โดยที่ BufferOverflow.DROP_OLDEST เป็นตัวเลือกที่นิยมสำหรับอีเวนต์ การฝึกฝนตัวดำเนินการเหล่านี้เชิงลึกอยู่ใน โมดูลสัมภาษณ์ Kotlin coroutines และ Flow

ข้อผิดพลาดที่ทำให้ผู้สมัครตกรอบ

การใช้ StateFlow สำหรับอีเวนต์การนำทางหรือ snackbar เพราะมัน replay ค่าล่าสุดให้ collector ใหม่ อีเวนต์จึงยิงซ้ำหลังการหมุนจอ และผู้ใช้จะถูกนำทางซ้ำสองครั้ง ควรเลือกใช้ SharedFlow ที่ตั้ง replay = 0 หรือออกแบบอีเวนต์แบบครั้งเดียว (one-shot) ที่ถูกล้างหลังจากจัดการเสร็จ

ซอร์สโค้ดของ kotlinx.coroutines และ เอกสารอ้างอิงแพ็กเกจ flow บน GitHub คุ้มค่าที่จะไล่อ่านก่อนการสัมภาษณ์ เพราะ KDoc ของ StateFlow และ SharedFlow ระบุการรับประกันเรื่อง conflation และ replay ไว้อย่างแม่นยำ

บทสรุป

ความต่างระหว่าง Kotlin Flow vs StateFlow vs SharedFlow ตอบแทนการใช้ภาษาที่แม่นยำในการสัมภาษณ์ ประเด็นสำคัญที่ควรจดจำ:

  • Flow เป็น cold: producer จะรันใหม่สำหรับทุก collector จึงเหมาะกับไปป์ไลน์ข้อมูลแบบตามความต้องการ
  • StateFlow เป็น hot เก็บค่าไว้เสมอ อีกทั้งทำ conflate และตัดค่าซ้ำ จึงเป็นเครื่องมือสำหรับสถานะ UI
  • SharedFlow เป็น hot ที่ไม่จำเป็นต้องมีค่าเริ่มต้น จึงเป็นตัวเลือกที่ถูกต้องสำหรับอีเวนต์ที่เกิดครั้งเดียว
  • แปลง cold เป็น hot ด้วย stateIn หรือ shareIn และใช้ SharingStarted.WhileSubscribed(5_000) เพื่อให้รอดจากการหมุนจอ
  • collect ด้วย repeatOnLifecycle(STARTED) หรือ collectAsStateWithLifecycle() เพื่อหลีกเลี่ยงงานเบื้องหลังและการ leak
  • อย่าส่งอีเวนต์ผ่าน StateFlow เด็ดขาด เพราะพฤติกรรม replay ของมันจะยิงซ้ำหลังการเปลี่ยน configuration

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#android
#kotlin
#coroutines
#flow
#interview

แชร์

บทความที่เกี่ยวข้อง