Luồng Dữ Liệu Một Chiều — Nó Là Gì, UDF trong Android và iOS

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

Hiểu Luồng Dữ Liệu Một Chiều là gì — một luồng dữ liệu đơn hướng, một mẫu kiến trúc nơi dữ liệu di chuyển theo vòng lặp khép kín State → View → Intent → Reducer → State mà không có vòng phản hồi. Khác với ràng buộc hai chiều, UDF đảm bảo rằng các thay đổi trạng thái chỉ xảy ra thông qua các hành động rõ ràng (Intent/Event), làm cho luồng dữ liệu có thể dự đoán và theo dõi được. Theo Google I/O 2024, UDF là kiến trúc được khuyến nghị cho các ứng dụng Jetpack Compose và SwiftUI với logic nghiệp vụ có độ phức tạp trung bình và cao.

Những Điểm Chính

  • Luồng Dữ Liệu Một Chiều (UDF) — một mẫu kiến trúc nơi trạng thái thay đổi theo đúng chu trình: đầu vào người dùng → Intent → Reducer → State mới → kết xuất lại View.
  • Trên Android, UDF được triển khai qua ViewModel + StateFlow + xử lý Intent; trên iOS — qua @Observable + mẫu Reducer (Composable Architecture).
  • Google khuyến nghị UDF là kiến trúc chính cho Jetpack Compose, bắt đầu từ tài liệu năm 2023.
  • UDF loại bỏ vấn đề vòng lặp vô hạn của Ràng buộc Hai chiều thông qua Nguồn Sự Thật Duy Nhất (Single Source of Truth).
  • Nhược điểm chính là nhiều mã soạn sẵn hơn so với ràng buộc hai chiều (State, Intent, Reducer, Effect).

Luồng Dữ Liệu Một Chiều là gì?

Luồng Dữ Liệu Một Chiều (UDF) là một mẫu kiến trúc nơi dữ liệu di chuyển theo một hướng trong một vòng lặp khép kín, loại bỏ các vòng phản hồi giữa View và Model. Khác với Ràng buộc Hai chiều, nơi một thay đổi trong UI ngay lập tức cập nhật model, UDF yêu cầu một hành động rõ ràng (Intent, Event, Action) cho mỗi thay đổi trạng thái. Điều này làm cho luồng dữ liệu hoàn toàn có thể dự đoán: tại bất kỳ thời điểm nào, có thể xác định hành động nào đã dẫn đến trạng thái hiện tại.

Khái niệm UDF đến từ các framework web — Redux (JavaScript, 2015) và Elm (2012) — và đã được điều chỉnh cho phát triển di động. Theo Google I/O 2024, UDF đã trở thành kiến trúc được khuyến nghị cho Jetpack Compose, thay thế MVVM cổ điển với LiveData. Trên iOS, một cách tiếp cận tương tự được triển khai trong The Composable Architecture (TCA) bởi Point-Free, được hơn 15% nhà phát triển iOS sử dụng theo Khảo sát Cộng đồng Swift (2024).

Ưu điểm chính của UDF là Nguồn Sự Thật Duy Nhất (SSOT): toàn bộ trạng thái ứng dụng được lưu trữ tại một nơi và được sửa đổi thông qua các thao tác được xác định nghiêm ngặt. Điều này đơn giản hóa việc gỡ lỗi, kiểm thử và tái tạo lỗi, vì mọi thay đổi trạng thái đều được ghi lại và có thể tái tạo bằng cách gửi lại cùng một Intents.

UDF hoạt động như thế nào: chu trình State → View → Intent → Reducer

Chu trình UDF cơ bản bao gồm bốn bước: State (trạng thái hiện tại) được kết xuất trong View; người dùng thực hiện một hành động trở thành Intent; Intent được xử lý trong một Reducer (hàm thuần túy), tạo ra một State mới; trạng thái mới được chuyển đến View để kết xuất lại. Chu trình này lặp lại ở mỗi sự kiện người dùng hoặc hệ thống.

Mỗi phần tử của chu trình có trách nhiệm nghiêm ngặt: State — một đối tượng bất biến mô tả trạng thái màn hình tại một thời điểm cụ thể; View — một hàm kết xuất State; Intent — một giá trị mô tả ý định của người dùng (ví dụ: LoginIntent.Submit); Reducer — một hàm thuần túy không có tác dụng phụ, nhận State hiện tại và Intent và trả về một State mới. Các tác dụng phụ (yêu cầu mạng, thao tác cơ sở dữ liệu) được chuyển đến một lớp Middleware hoặc Effect riêng biệt.

Theo bài viết Google Android Architecture (2024), tính thuần túy của Reducer là một yêu cầu chính: nếu Reducer chứa một lệnh gọi mạng hoặc ghi cơ sở dữ liệu, việc kiểm thử và gỡ lỗi luồng dữ liệu trở nên bất khả thi. Tất cả các tác dụng phụ phải được thực thi trong một coroutine ViewModel hoặc Swift Task trước khi gọi Reducer, và kết quả phải được gửi dưới dạng một Intent mới.

UDF trong Android: ViewModel + StateFlow + Intent

Trên Android, việc triển khai UDF được xây dựng trên ba thành phần Jetpack: ViewModel quản lý vòng đời, StateFlow cung cấp một luồng trạng thái phản ứng, Intent (sealed class) mô tả tất cả các hành động có thể có của người dùng. View đăng ký StateFlow qua collectAsState() trong Compose hoặc observe() trong hệ thống 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) }
    )
}

Ví dụ minh họa chu trình UDF hoàn chỉnh trong Android: LoginIntent mô tả tất cả các hành động có thể (thay đổi email, thay đổi mật khẩu, gửi biểu mẫu), LoginState là trạng thái bất biến, LoginViewModel xử lý Intents và cập nhật StateFlow, và màn hình Compose đăng ký trạng thái qua collectAsState(). Mọi thay đổi trạng thái đều là kết quả của việc xử lý một Intent cụ thể, làm cho luồng dữ liệu hoàn toàn minh bạch.

UDF trong iOS: TCA và mẫu Observable

Trên iOS, UDF được triển khai thông qua The Composable Architecture (TCA) bởi Point-Free hoặc mẫu Observable gốc với iOS 17+. TCA cung cấp một chu trình có sẵn State + Action + Reducer + Store, nơi Store là nguồn sự thật duy nhất, và View đăng ký các thay đổi qua @Observable hoặc 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("Đăng nhập") { viewStore.send(.submit) }
            }
        }
    }
}

loginReducer là một hàm thuần túy: nó không thực hiện yêu cầu mạng trực tiếp mà trả về một Effect sẽ được thực thi bởi môi trường TCA. Điều này cho phép kiểm thử bộ rút gọn một cách cô lập bằng cách thay thế các hiệu ứng trong bài kiểm thử. View đăng ký các thay đổi của Store qua WithViewStore và gửi Actions qua send(). TCA tự động xử lý việc hủy bỏ các hiệu ứng khi Store bị phá hủy, ngăn chặn rò rỉ bộ nhớ.

UDF so với MVVM: Sự khác biệt là gì?

MVVMUDF thường bị nhầm lẫn, nhưng có một sự khác biệt cơ bản giữa chúng. MVVM là một mẫu cấu trúc phân tách mã thành ba lớp (Model, View, ViewModel) nhưng không xác định hướng của luồng dữ liệu. UDF là một mẫu hành vi mô tả cách dữ liệu di chuyển trong cấu trúc đó. Trong MVVM với LiveData, cả ràng buộc hai chiều và luồng một chiều đều có thể — UDF thêm các quy tắc xử lý Intent nghiêm ngặt vào MVVM.

Theo tài liệu Android Developers (2024), kiến trúc được khuyến nghị cho Compose là UDF bên trong MVVM: ViewModel lưu trữ State và xử lý Intents, View đăng ký State và gửi Intents. Google khuyến nghị MVVM cổ điển với Ràng buộc Hai chiều qua DataBinding chỉ cho các màn hình đơn giản không có logic nghiệp vụ. Đối với Jetpack Compose, kịch bản chính là UDF với xử lý sự kiện rõ ràng.

Bảng so sánh:

Đặc điểmMVVM (cổ điển)MVVM + UDF
Luồng dữ liệuKhông xác địnhNghiêm ngặt một chiều
Thay đổi trạng tháiTrực tiếp qua setText()Chỉ qua Intent → Reducer
Nguồn Sự Thật Duy NhấtKhông
Khả năng kiểm thử ReducerThấpCao (hàm thuần túy)
Khuyến nghị của GoogleCách tiếp cận cũChính cho Compose

Những lỗi thường gặp khi triển khai UDF

Lỗi phổ biến nhất là tác dụng phụ bên trong Reducer. Các nhà phát triển quen với MVVM đặt các yêu cầu mạng trực tiếp trong trình xử lý Intent, làm cho Reducer không thuần túy và phá vỡ khả năng kiểm thử. Tất cả các hiệu ứng phải được trả về dưới dạng một giá trị (Effect / SideEffect) và được thực thi bởi cơ sở hạ tầng framework. Trên Android, coroutine trong ViewModel được sử dụng cho việc này; trong TCA — Effect.run.

Lỗi thứ hai là Intents quá chi tiết. Mỗi lần gõ phím, di chuyển thanh trượt và thay đổi văn bản tạo ra một Intent riêng biệt. Đối với các trường nhập liệu, điều này là quá mức — trong những trường hợp như vậy, có thể chấp nhận sử dụng Binding với luồng một chiều trong biểu mẫu (trạng thái cục bộ) và chỉ gửi một Intent toàn cục cho các hành động quan trọng (gửi, điều hướng).

Lỗi thứ ba là thiếu xử lý hủy bỏ hiệu ứng. Nếu người dùng rời khỏi màn hình trong khi một coroutine hoặc Task vẫn đang thực thi, kết quả có thể được áp dụng cho một View đã bị phá hủy. Trên Android, sử dụng viewModelScope.cancel() hoặc takeWhileActive(); trong TCA, các hiệu ứng tự động bị hủy khi Store bị phá hủy. Theo Google Issue Tracker (2024), rò rỉ từ các coroutine chưa hoàn thành nằm trong top 5 nguyên nhân gây sập ứng dụng Compose.

Câu hỏi thường gặp

UDF khác với MVI như thế nào?

MVI (Model-View-Intent) là một trường hợp cụ thể của UDF với ba yếu tố bắt buộc: Intent (ý định), Model (trạng thái), View (hiển thị). Sự khác biệt chính là trong MVI, mỗi trạng thái màn hình được mô tả bằng một cấu trúc bất biến duy nhất (Sealed class), và View là một hàm thuần túy từ Model đến UI. UDF là một thuật ngữ rộng hơn mô tả bất kỳ luồng một chiều nào, bao gồm Redux và Elm. Trong tài liệu của Google, thuật ngữ UDF được sử dụng như tên gọi chung, còn MVI là một triển khai cụ thể.

Khi nào UDF là quá mức?

UDF là quá mức đối với các màn hình có một trường nhập liệu duy nhất không có xác thực, các trang tĩnh và màn hình giữ chỗ. Nếu một màn hình không có logic nghiệp vụ và trạng thái của nó không phụ thuộc vào hành động của người dùng, UDF thêm mã không cần thiết mà không có lợi ích. Đối với những kịch bản như vậy, ràng buộc một chiều hoặc @State đơn giản trong SwiftUI là đủ. UDF được chứng minh khi số lượng trạng thái màn hình có thể vượt quá 3–4 và/hoặc có các tác dụng phụ.

Làm thế nào để kiểm thử UDF?

Vì Reducer là một hàm thuần túy, việc kiểm thử nó chỉ đơn giản là gọi nó với các kết hợp khác nhau của State và Intent và kiểm tra State và Effect kết quả. Trên Android, sử dụng Turbine để kiểm thử StateFlow: gửi một Intent, kiểm tra lần phát State tiếp theo. Trong TCA, có một TestStore tích hợp tự động xác minh rằng sau một Action, chỉ các trường State dự kiến thay đổi và chỉ các Effects dự kiến được thực thi.

Có thể kết hợp UDF và Ràng buộc Hai chiều không?

Có, kết hợp chúng là chấp nhận được và thường là tối ưu. Đối với các trường nhập liệu trong biểu mẫu, hãy sử dụng Ràng buộc Hai chiều cục bộ (hoặc Binding trong SwiftUI) để tránh tạo Intent cho mỗi lần gõ phím. Khi gửi biểu mẫu, hãy gửi một Intent duy nhất với dữ liệu đã thu thập, được xử lý bởi Reducer. Cách tiếp cận kết hợp này — UDF toàn cục với Ràng buộc Hai chiều cục bộ — được sử dụng trong 70% ứng dụng SwiftUI thương mại (dữ liệu Khảo sát Cộng đồng Swift 2024).

UDF, Redux và Elm có điểm gì chung?

Cả ba mẫu đều triển khai luồng dữ liệu một chiều với một nguồn sự thật duy nhất. Elm (2012) — một ngôn ngữ hàm — lần đầu tiên giới thiệu chu trình thuần túy Model → View → Update. Redux (2015) đã điều chỉnh Elm cho JavaScript với các khái niệm Store, Reducer và Action. UDF là sự tổng quát hóa những ý tưởng này cho phát triển di động. Cả ba cách tiếp cận đều đảm bảo khả năng dự đoán của các thay đổi thông qua các cập nhật trạng thái nguyên tử.

Tổng kết

  • Luồng Dữ Liệu Một Chiều (UDF) là một mẫu với chu trình một chiều State → View → Intent → Reducer, đảm bảo các thay đổi trạng thái có thể dự đoán.
  • Trên Android, UDF được triển khai qua ViewModel + StateFlow + sealed class Intent; trên iOS — qua TCA (Reducer + Store) hoặc Observable gốc.
  • Google khuyến nghị UDF là kiến trúc chính cho Jetpack Compose, bắt đầu từ năm 2023.
  • Reducer là một hàm thuần túy không có tác dụng phụ; tất cả các yêu cầu mạng và thao tác cơ sở dữ liệu được chuyển đến lớp Effect.
  • UDF loại bỏ vấn đề vòng lặp vô hạn của Ràng buộc Hai chiều thông qua xử lý Intent rõ ràng và Nguồn Sự Thật Duy Nhất.
  • Các rủi ro chính — tác dụng phụ trong Reducer, Intents quá chi tiết và coroutine chưa hoàn thành.
  • Cách tiếp cận kết hợp (Ràng buộc Hai chiều cục bộ trong biểu mẫu + UDF toàn cục) là tối ưu cho hầu hết các ứng dụng thương mại.

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