การไหลของข้อมูลทางเดียว — คืออะไร, UDF ใน Android และ iOS

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

ทำความเข้าใจว่า การไหลของข้อมูลทางเดียว (Unidirectional Data Flow) คืออะไร — สตรีมข้อมูลทิศทางเดียว รูปแบบสถาปัตยกรรมที่ข้อมูลเคลื่อนที่ในลูปปิด State → View → Intent → Reducer → State โดยไม่มีลูปป้อนกลับ แตกต่างจากการผูกมัดสองทาง UDF รับประกันว่าการเปลี่ยนแปลงสถานะเกิดขึ้นผ่านการกระทำที่ชัดเจน (Intent/Event) เท่านั้น ทำให้การไหลของข้อมูลคาดเดาได้และติดตามได้ ตามข้อมูลของ Google I/O 2024 UDF เป็นสถาปัตยกรรมที่แนะนำสำหรับแอปพลิเคชัน Jetpack Compose และ SwiftUI ที่มีตรรกะทางธุรกิจระดับความซับซ้อนปานกลางและสูง

ประเด็นสำคัญ

  • การไหลของข้อมูลทางเดียว (UDF) — รูปแบบสถาปัตยกรรมที่สถานะเปลี่ยนแปลงตามวงจรอย่างเคร่งครัด: การป้อนข้อมูลของผู้ใช้ → Intent → Reducer → สถานะใหม่ → การเรนเดอร์ View ใหม่
  • บน Android UDF ถูกนำไปใช้ผ่าน ViewModel + StateFlow + การจัดการ Intent; บน iOS — ผ่าน @Observable + รูปแบบ Reducer (Composable Architecture)
  • Google แนะนำ UDF เป็นสถาปัตยกรรมหลักสำหรับ Jetpack Compose ตั้งแต่เอกสารประกอบปี 2023
  • UDF กำจัดปัญหาลูปอนันต์ของการผูกมัดสองทางผ่านแหล่งความจริงเดียว (Single Source of Truth)
  • ข้อเสียหลักคือโค้ดเทมเพลตที่มากขึ้นเมื่อเทียบกับการผูกมัดสองทาง (State, Intent, Reducer, Effect)

การไหลของข้อมูลทางเดียวคืออะไร?

การไหลของข้อมูลทางเดียว (UDF) เป็นรูปแบบสถาปัตยกรรมที่ข้อมูลเคลื่อนที่ในทิศทางเดียวในลูปปิด กำจัดลูปป้อนกลับระหว่าง View และ Model แตกต่างจากการผูกมัดสองทาง ที่การเปลี่ยนแปลงใน UI อัปเดตโมเดลทันที UDF ต้องการการกระทำที่ชัดเจน (Intent, Event, Action) สำหรับทุกการเปลี่ยนแปลงสถานะ ทำให้การไหลของข้อมูลคาดเดาได้อย่างสมบูรณ์: ในเวลาใดก็ได้ สามารถระบุได้ว่าการกระทำใดนำไปสู่สถานะปัจจุบัน

แนวคิดของ UDF มาจากเฟรมเวิร์กเว็บ — Redux (JavaScript, 2015) และ Elm (2012) — และถูกปรับใช้สำหรับการพัฒนามือถือ ตามข้อมูลของ Google I/O 2024 UDF ได้กลายเป็นสถาปัตยกรรมที่แนะนำสำหรับ Jetpack Compose แทนที่ MVVM แบบคลาสสิกด้วย LiveData บน iOS มีการใช้แนวทางที่คล้ายกันใน The Composable Architecture (TCA) โดย Point-Free ซึ่งนักพัฒนา iOS กว่า 15% ใช้ตามข้อมูลของ Swift Community Survey (2024)

ข้อได้เปรียบหลักของ UDF คือ แหล่งความจริงเดียว (SSOT): สถานะทั้งหมดของแอปพลิเคชันถูกจัดเก็บในที่เดียวและแก้ไขผ่านการดำเนินการที่กำหนดไว้อย่างเคร่งครัด ซึ่งทำให้การดีบัก การทดสอบ และการทำซ้ำข้อบกพร่องง่ายขึ้น เนื่องจากการเปลี่ยนแปลงสถานะทุกครั้งถูกบันทึกและสามารถทำซ้ำได้โดยการส่ง Intent เดิมอีกครั้ง

UDF ทำงานอย่างไร: วงจร State → View → Intent → Reducer

วงจร UDF พื้นฐานประกอบด้วยสี่ขั้นตอน: State (สถานะปัจจุบัน) ถูกเรนเดอร์ใน View; ผู้ใช้ดำเนินการที่กลายเป็น Intent; Intent ถูกประมวลผลใน Reducer (ฟังก์ชันบริสุทธิ์) ซึ่งสร้าง State ใหม่; สถานะใหม่ถูกส่งไปยัง View เพื่อเรนเดอร์ใหม่ วงจรนี้ทำซ้ำในทุกเหตุการณ์ของผู้ใช้หรือระบบ

แต่ละองค์ประกอบของวงจรมีความรับผิดชอบที่เคร่งครัด: State — วัตถุที่ไม่สามารถเปลี่ยนแปลงได้ซึ่งอธิบายสถานะหน้าจอในขณะที่เฉพาะเจาะจง; View — ฟังก์ชันที่เรนเดอร์ State; Intent — ค่าที่อธิบายความตั้งใจของผู้ใช้ (เช่น LoginIntent.Submit); Reducer — ฟังก์ชันบริสุทธิ์ที่ไม่มีผลข้างเคียง รับ State ปัจจุบันและ Intent และส่งคืน State ใหม่ ผลข้างเคียง (คำขอเครือข่าย, การดำเนินการฐานข้อมูล) ถูกย้ายไปยังเลเยอร์ Middleware หรือ Effect ที่แยกต่างหาก

ตามบทความ Google Android Architecture (2024) ความบริสุทธิ์ของ Reducer เป็นข้อกำหนดสำคัญ: หาก Reducer มีการเรียกเครือข่ายหรือการเขียนฐานข้อมูล การทดสอบและดีบักการไหลของข้อมูลจะเป็นไปไม่ได้ ผลข้างเคียงทั้งหมดต้องดำเนินการใน coroutine ของ ViewModel หรือ Swift Task ก่อนเรียก Reducer และผลลัพธ์ต้องถูกส่งเป็น Intent ใหม่

UDF ใน Android: ViewModel + StateFlow + Intent

บน Android การนำ UDF ไปใช้สร้างขึ้นบนองค์ประกอบ Jetpack สามส่วน: ViewModel จัดการวงจรชีวิต, StateFlow ให้สตรีมสถานะแบบรีแอกทีฟ, Intent (sealed class) อธิบายการกระทำที่เป็นไปได้ทั้งหมดของผู้ใช้ View สมัครรับ StateFlow ผ่าน collectAsState() ใน Compose หรือ observe() ในระบบ View

Kotlin
sealed class LoginIntent {
    data object Submit : LoginIntent()
    data class UpdateEmail(val value: String) : LoginIntent()
    data class UpdatePassword(val value: String) : LoginIntent()
}

data class LoginState(
    val email: String = "",
    val password: String = "",
    val isLoading: Boolean = false,
    val error: String? = null
)

class LoginViewModel : ViewModel() {
    private val _state = MutableStateFlow(LoginState())
    val state: StateFlow<LoginState> = _state.asStateFlow()

    fun onIntent(intent: LoginIntent) {
        when (intent) {
            is LoginIntent.UpdateEmail -> {
                _state.update { it.copy(email = intent.value) }
            }
            is LoginIntent.UpdatePassword -> {
                _state.update { it.copy(password = intent.value) }
            }
            is LoginIntent.Submit -> {
                _state.update { it.copy(isLoading = true, error = null) }
                loginUseCase(_state.value.email, _state.value.password)
                    .onSuccess {
                        _state.update { it.copy(isLoading = false) }
                    }
                    .onFailure { e ->
                        _state.update { it.copy(isLoading = false, error = e.message) }
                    }
            }
        }
    }
}

@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val state by viewModel.state.collectAsState()

    LoginForm(
        email = state.email,
        onEmailChange = { viewModel.onIntent(LoginIntent.UpdateEmail(it)) },
        password = state.password,
        onPasswordChange = { viewModel.onIntent(LoginIntent.UpdatePassword(it)) },
        isLoading = state.isLoading,
        onSubmit = { viewModel.onIntent(LoginIntent.Submit) }
    )
}

ตัวอย่างแสดงวงจร UDF ที่สมบูรณ์ใน Android: LoginIntent อธิบายการกระทำที่เป็นไปได้ทั้งหมด (การเปลี่ยนแปลงอีเมล, การเปลี่ยนแปลงรหัสผ่าน, การส่งแบบฟอร์ม), LoginState เป็นสถานะที่ไม่สามารถเปลี่ยนแปลงได้, LoginViewModel ประมวลผล Intents และอัปเดต StateFlow, และหน้าจอ Compose สมัครรับสถานะผ่าน collectAsState() ทุกการเปลี่ยนแปลงสถานะเป็นผลลัพธ์ของการประมวลผล Intent ที่เฉพาะเจาะจง ทำให้การไหลของข้อมูลโปร่งใสอย่างสมบูรณ์

UDF ใน iOS: TCA และรูปแบบ Observable

บน iOS UDF ถูกนำไปใช้ผ่าน The Composable Architecture (TCA) โดย Point-Free หรือรูปแบบ Observable ดั้งเดิมกับ iOS 17+ TCA มีวงจรพร้อมใช้ของ State + Action + Reducer + Store โดย Store เป็นแหล่งความจริงเดียว และ View สมัครรับการเปลี่ยนแปลงผ่าน @Observable หรือ ObservableObject

Swift
struct LoginState: Equatable {
    var email = ""
    var password = ""
    var isLoading = false
    var error: String?
}

enum LoginAction {
    case emailChanged(String)
    case passwordChanged(String)
    case submit
    case loginResponse(Result<User, Error>)
}

let loginReducer = Reducer<LoginState, LoginAction> { state, action in
    switch action {
    case .emailChanged(let email):
        state.email = email
        return .none
    case .passwordChanged(let password):
        state.password = password
        return .none
    case .submit:
        state.isLoading = true
        state.error = nil
        return .run { send in
            let result = await loginUseCase(state.email, state.password)
            await send(.loginResponse(result))
        }
    case .loginResponse(.success):
        state.isLoading = false
        return .none
    case .loginResponse(.failure(let error)):
        state.isLoading = false
        state.error = error.localizedDescription
        return .none
    }
}

struct LoginView: View {
    let store: StoreOf<LoginReducer>

    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            Form {
                TextField("Email", text: viewStore.binding(get: \.email, send: { .emailChanged($0) }))
                SecureField("Password", text: viewStore.binding(get: \.password, send: { .passwordChanged($0) }))
                Button("เข้าสู่ระบบ") { viewStore.send(.submit) }
            }
        }
    }
}

loginReducer เป็นฟังก์ชันบริสุทธิ์: มันไม่ดำเนินการขอเครือข่ายโดยตรงแต่ส่งคืน Effect ที่จะถูกดำเนินการโดยรันไทม์ TCA ซึ่งช่วยให้ทดสอบรีดิวเซอร์อย่างแยกส่วนโดยการแทนที่เอฟเฟกต์ในการทดสอบ View สมัครรับการเปลี่ยนแปลงของ Store ผ่าน WithViewStore และส่ง Actions ผ่าน send() TCA จัดการการยกเลิกเอฟเฟกต์โดยอัตโนมัติเมื่อ Store ถูกทำลาย ป้องกันการรั่วไหลของหน่วยความจำ

UDF เทียบกับ MVVM: แตกต่างกันอย่างไร?

MVVM และ UDF มักสับสน แต่มีความแตกต่างพื้นฐานระหว่างพวกเขา MVVM เป็นรูปแบบโครงสร้างที่แยกโค้ดออกเป็นสามเลเยอร์ (Model, View, ViewModel) แต่ไม่ได้กำหนดทิศทางการไหลของข้อมูล UDF เป็นรูปแบบพฤติกรรมที่อธิบายว่าข้อมูลเคลื่อนที่ภายในโครงสร้างนั้นอย่างไร ใน MVVM กับ LiveData ทั้งการผูกมัดสองทางและการไหลทางเดียวเป็นไปได้ — UDF เพิ่มกฎการประมวลผล Intent ที่เคร่งครัดให้กับ MVVM

ตามเอกสารประกอบ Android Developers (2024) สถาปัตยกรรมที่แนะนำสำหรับ Compose คือ UDF ภายใน MVVM: ViewModel จัดเก็บ State และประมวลผล Intents, View สมัครรับ State และส่ง Intents Google แนะนำ MVVM แบบคลาสสิกกับการผูกมัดสองทางผ่าน DataBinding เฉพาะสำหรับหน้าจอที่ไม่มีตรรกะทางธุรกิจเท่านั้น สำหรับ Jetpack Compose สถานการณ์หลักคือ UDF ที่มีการจัดการเหตุการณ์อย่างชัดเจน

ตารางเปรียบเทียบ:

ลักษณะMVVM (คลาสสิก)MVVM + UDF
การไหลของข้อมูลไม่ได้กำหนดทางเดียวอย่างเคร่งครัด
การเปลี่ยนแปลงสถานะโดยตรงผ่าน setText()ผ่าน Intent → Reducer เท่านั้น
แหล่งความจริงเดียวไม่ใช่
ความสามารถในการทดสอบ Reducerต่ำสูง (ฟังก์ชันบริสุทธิ์)
คำแนะนำของ Googleแนวทางเดิมหลักสำหรับ Compose

ข้อผิดพลาดทั่วไปเมื่อนำ UDF ไปใช้

ข้อผิดพลาดที่พบบ่อยที่สุดคือ ผลข้างเคียงภายใน Reducer นักพัฒนาที่คุ้นเคยกับ MVVM วางคำขอเครือข่ายโดยตรงในตัวจัดการ Intent ทำให้ Reducer ไม่บริสุทธิ์และทำลายความสามารถในการทดสอบ เอฟเฟกต์ทั้งหมดต้องถูกส่งคืนเป็นค่า (Effect / SideEffect) และดำเนินการโดยโครงสร้างพื้นฐานของเฟรมเวิร์ก บน Android ใช้ coroutine ใน ViewModel สำหรับสิ่งนี้; ใน TCA — Effect.run

ข้อผิดพลาดที่สองคือ Intents ที่ละเอียดเกินไป การกดแป้นพิมพ์ทุกครั้ง การเคลื่อนไหวของแถบเลื่อน และการเปลี่ยนแปลงข้อความสร้าง Intent แยกต่างหาก สำหรับฟิลด์ป้อนข้อมูลนี่มากเกินไป — ในกรณีเช่นนี้ การใช้ Binding กับการไหลทางเดียวภายในแบบฟอร์ม (สถานะท้องถิ่น) เป็นที่ยอมรับ และส่ง Intent ทั่วโลกเฉพาะสำหรับการกระทำที่สำคัญ (ส่ง, นำทาง) เท่านั้น

ข้อผิดพลาดที่สามคือ การขาดการจัดการยกเลิกเอฟเฟกต์ หากผู้ใช้ออกจากหน้าจอในขณะที่ coroutine หรือ Task ยังคงทำงานอยู่ ผลลัพธ์อาจถูกนำไปใช้กับ View ที่ถูกทำลายไปแล้ว บน Android ใช้ viewModelScope.cancel() หรือ takeWhileActive(); ใน TCA เอฟเฟกต์จะถูกยกเลิกโดยอัตโนมัติเมื่อ Store ถูกทำลาย ตาม Google Issue Tracker (2024) การรั่วไหลจาก coroutine ที่ไม่สมบูรณ์อยู่ใน 5 อันดับแรกของสาเหตุการหยุดทำงานในแอปพลิเคชัน Compose

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

UDF แตกต่างจาก MVI อย่างไร?

MVI (Model-View-Intent) เป็นกรณีเฉพาะของ UDF ที่มีองค์ประกอบบังคับสามอย่าง: Intent (ความตั้งใจ), Model (สถานะ), View (การแสดงผล) ความแตกต่างหลักคือใน MVI แต่ละสถานะหน้าจอถูกอธิบายด้วยโครงสร้างที่ไม่สามารถเปลี่ยนแปลงได้เดียว (Sealed class) และ View เป็นฟังก์ชันบริสุทธิ์จาก Model ไปยัง UI UDF เป็นคำที่กว้างกว่าซึ่งอธิบายการไหลทางเดียวใด ๆ รวมถึง Redux และ Elm ในเอกสารประกอบของ Google คำว่า UDF ถูกใช้เป็นชื่อทั่วไป ในขณะที่ MVI เป็นการนำไปใช้เฉพาะ

เมื่อใดที่ UDF มากเกินไป?

UDF มากเกินไปสำหรับหน้าจอที่มีฟิลด์ป้อนข้อมูลเดียวที่ไม่มีการตรวจสอบ หน้าคงที่ และหน้าจอตัวยึดตำแหน่ง หากหน้าจอไม่มีตรรกะทางธุรกิจและสถานะของมันไม่ขึ้นอยู่กับการกระทำของผู้ใช้ UDF จะเพิ่มโค้ดที่ไม่จำเป็นโดยไม่มีประโยชน์ สำหรับสถานการณ์เช่นนี้ การผูกมัดทางเดียวหรือ @State อย่างง่ายใน SwiftUI ก็เพียงพอ UDF จะสมเหตุสมผลเมื่อจำนวนสถานะหน้าจอที่เป็นไปได้เกิน 3–4 และ/หรือมีผลข้างเคียง

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

เนื่องจาก Reducer เป็นฟังก์ชันบริสุทธิ์ การทดสอบจึงลดลงเป็นการเรียกด้วยชุดค่าผสมต่าง ๆ ของ State และ Intent และตรวจสอบ State และ Effect ที่ได้ บน Android ใช้ Turbine เพื่อทดสอบ StateFlow: ส่ง Intent ตรวจสอบการส่งสถานะครั้งถัดไป ใน TCA มี TestStore ในตัวที่ตรวจสอบโดยอัตโนมัติว่าหลังจาก Action เฉพาะฟิลด์ State ที่คาดหวังเท่านั้นที่เปลี่ยนแปลงและเฉพาะ Effects ที่คาดหวังเท่านั้นที่ถูกดำเนินการ

สามารถรวม UDF และการผูกมัดสองทางได้หรือไม่?

ได้ การรวมกันนั้นยอมรับได้และมักจะเหมาะสมที่สุด สำหรับฟิลด์ป้อนข้อมูลภายในแบบฟอร์ม ให้ใช้การผูกมัดสองทางท้องถิ่น (หรือ Binding ใน SwiftUI) เพื่อหลีกเลี่ยงการสร้าง Intent ทุกครั้งที่กดแป้นพิมพ์ เมื่อส่งแบบฟอร์ม ให้ส่ง Intent เดียวพร้อมข้อมูลที่รวบรวม ซึ่งประมวลผลโดย Reducer แนวทางแบบผสมนี้ — UDF ทั่วโลกกับการผูกมัดสองทางท้องถิ่น — ใช้ใน 70% ของแอปพลิเคชัน SwiftIO เชิงพาณิชย์ (ข้อมูล Swift Community Survey 2024)

UDF, Redux และ Elm มีอะไรที่เหมือนกัน?

ทั้งสามรูปแบบใช้การไหลของข้อมูลทางเดียวกับแหล่งความจริงเดียว Elm (2012) — ภาษาฟังก์ชันนัล — นำเสนอวงจรบริสุทธิ์ Model → View → Update เป็นครั้งแรก Redux (2015) ปรับ Elm สำหรับ JavaScript ด้วยแนวคิดของ Store, Reducer และ Action UDF เป็นการสรุปแนวคิดเหล่านี้สำหรับการพัฒนามือถือ ทั้งสามแนวทางรับประกันความสามารถในการคาดเดาของการเปลี่ยนแปลงผ่านการอัปเดตสถานะแบบอะตอมมิก

สรุป

  • การไหลของข้อมูลทางเดียว (UDF) เป็นรูปแบบที่มีวงจรทางเดียว State → View → Intent → Reducer ซึ่งรับประกันการเปลี่ยนแปลงสถานะที่คาดเดาได้
  • บน Android UDF ถูกนำไปใช้ผ่าน ViewModel + StateFlow + sealed class Intent; บน iOS — ผ่าน TCA (Reducer + Store) หรือ Observable ดั้งเดิม
  • Google แนะนำ UDF เป็นสถาปัตยกรรมหลักสำหรับ Jetpack Compose ตั้งแต่ปี 2023
  • Reducer เป็นฟังก์ชันบริสุทธิ์ที่ไม่มีผลข้างเคียง; คำขอเครือข่ายและการดำเนินการฐานข้อมูลทั้งหมดถูกย้ายไปยังเลเยอร์ Effect
  • UDF กำจัดปัญหาลูปอนันต์ของการผูกมัดสองทางผ่านการจัดการ Intent ที่ชัดเจนและแหล่งความจริงเดียว
  • ความเสี่ยงหลัก — ผลข้างเคียงใน Reducer, Intents ที่ละเอียดเกินไป และ coroutine ที่ไม่สมบูรณ์
  • แนวทางแบบผสม (การผูกมัดสองทางท้องถิ่นในแบบฟอร์ม + UDF ทั่วโลก) เหมาะสมที่สุดสำหรับแอปพลิเคชันเชิงพาณิชย์ส่วนใหญ่

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

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

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

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