Unidirectional Data Flow — چیست، 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 با منطق تجاری متوسط و پیچیده است.

نکات اصلی

  • Unidirectional Data Flow (UDF) — الگوی معماری که در آن وضعیت به‌طور دقیق در چرخه تغییر می‌کند: ورودی کاربر → Intent → Reducer → State جدید → بازترسیم View.
  • در Android UDF از طریق ViewModel + StateFlow + پردازش Intent پیاده‌سازی می‌شود؛ در iOS — از طریق @Observable + الگوی Reducer (Composable Architecture).
  • Google از مستندات 2023 به بعد UDF را به عنوان معماری اصلی برای Jetpack Compose توصیه می‌کند.
  • UDF مشکل حلقه‌های بی‌نهایت Two-Way Binding را با منبع واحد حقیقت (Single Source of Truth) برطرف می‌کند.
  • عیب اصلی — کد قالبی بیشتر نسبت به اتصال دوطرفه (State, Intent, Reducer, Effect).

Unidirectional Data Flow چیست؟

Unidirectional Data Flow (UDF) — الگوی معماری است که در آن داده‌ها در یک جهت در امتداد یک چرخه بسته حرکت می‌کنند و بازخورد بین View و Model را حذف می‌کنند. برخلاف Two-Way Binding، که در آن تغییر در 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 پیاده‌سازی شده است که طبق نظرسنجی Swift Community (2024) بیش از 15٪ از توسعه‌دهندگان iOS از آن استفاده می‌کنند.

مزیت اصلی UDF — Single Source of Truth (SSOT): تمام وضعیت برنامه در یک مکان ذخیره می‌شود و از طریق عملیات کاملاً تعریف‌شده تغییر می‌کند. این کار اشکال‌زدایی، تست و بازتولید باگ‌ها را ساده می‌کند، زیرا هر تغییر وضعیت ثبت می‌شود و می‌توان با ارسال مجدد همان Intentها آن را بازتولید کرد.

UDF چگونه کار می‌کند: چرخه State → View → Intent → Reducer

چرخه پایه UDF از چهار مرحله تشکیل شده است: State (وضعیت فعلی) در View نمایش داده می‌شود؛ کاربر عملی انجام می‌دهد که به Intent (نیت) تبدیل می‌شود؛ Intent در Reducer (یک تابع خالص) پردازش می‌شود که State جدیدی ایجاد می‌کند؛ وضعیت جدید برای بازترسیم به View منتقل می‌شود. این چرخه در هر رویداد کاربری یا سیستمی تکرار می‌شود.

هر عنصر چرخه مسئولیت دقیقی دارد: State — یک شیء تغییرناپذیر (immutable) که وضعیت صفحه را در یک لحظه خاص توصیف می‌کند؛ View — تابعی که State را نمایش می‌دهد؛ Intent — مقداری که نیت کاربر را توصیف می‌کند (مثلاً LoginIntent.Submit)؛ Reducer — یک تابع خالص بدون عوارض جانبی که State و Intent فعلی را دریافت کرده و State جدیدی برمی‌گرداند. عوارض جانبی (درخواست‌های شبکه، پایگاه داده) به لایه جداگانه Middleware یا Effect منتقل می‌شوند.

طبق مقاله Google Android Architecture (2024)، خلوص Reducer یک نیاز کلیدی است: اگر Reducer شامل فراخوانی شبکه یا نوشتن در پایگاه داده باشد، تست و اشکال‌زدایی جریان داده غیرممکن می‌شود. همه عوارض جانبی باید قبل از فراخوانی Reducer در coroutine ViewModel یا Swift Task اجرا شوند و نتیجه به عنوان یک Intent جدید ارسال شود.

UDF در Android: ViewModel + StateFlow + Intent

در Android پیاده‌سازی UDF بر سه مؤلفه Jetpack استوار است: ViewModel چرخه حیات را مدیریت می‌کند، StateFlow جریان واکنشی وضعیت‌ها را فراهم می‌کند، Intent (sealed class) تمام اقدامات ممکن کاربر را توصیف می‌کند. View از طریق collectAsState() در Compose یا observe() در سیستم View در StateFlow مشترک می‌شود.

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 Intentها را پردازش کرده و StateFlow را به‌روز می‌کند، و صفحه Compose از طریق collectAsState() در state مشترک می‌شود. هر تغییر وضعیت نتیجه پردازش یک 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 از طریق WithViewStore در تغییرات Store مشترک شده و از طریق send() Action ارسال می‌کند. TCA به طور خودکار لغو اثرات را هنگام نابودی Store مدیریت می‌کند و از نشت حافظه جلوگیری می‌کند.

UDF در مقابل MVVM: تفاوت چیست؟

MVVM و UDF اغلب اشتباه گرفته می‌شوند، اما تفاوت اساسی بین آنها وجود دارد. MVVM یک الگوی ساختاری است که کد را به سه لایه (Model, View, ViewModel) تقسیم می‌کند، اما جهت جریان داده را مشخص نمی‌کند. UDF یک الگوی رفتاری است که نحوه حرکت داده‌ها را در این ساختار توصیف می‌کند. در MVVM با LiveData می‌تواند هم اتصال دوطرفه و هم جریان یک‌طرفه وجود داشته باشد — UDF قوانین سختگیرانه‌ای برای پردازش Intent به MVVM اضافه می‌کند.

طبق مستندات Android Developers (2024)، معماری توصیه‌شده برای Compose UDF در داخل MVVM است: ViewModel State را ذخیره کرده و Intentها را پردازش می‌کند، View در State مشترک شده و Intent ارسال می‌کند. Google MVVM کلاسیک با Two-Way Binding از طریق DataBinding را فقط برای صفحات ساده بدون منطق تجاری توصیه می‌کند. برای Jetpack Compose سناریوی اصلی UDF با پردازش صریح رویدادها است.

جدول مقایسه:

ویژگیMVVM (کلاسیک)MVVM + UDF
جریان دادهتعریف نشدهبه‌طور دقیق یک‌طرفه
تغییر وضعیتمستقیماً از طریق setText()فقط از طریق Intent → Reducer
Single Source of Truthخیربله
قابلیت تست کاهندهپایینبالا (تابع خالص)
توصیه Googleرویکرد قدیمیاصلی برای Compose

اشتباهات رایج در پیاده‌سازی UDF

رایج‌ترین اشتباه — عوارض جانبی در داخل Reducer. توسعه‌دهندگانی که به MVVM عادت دارند، درخواست‌های شبکه را مستقیماً در handler Intent قرار می‌دهند که Reducer را به تابعی ناخالص تبدیل کرده و قابلیت تست را از بین می‌برد. همه اثرات باید به عنوان مقدار (Effect / SideEffect) برگردانده شده و توسط زیرساخت فریم‌ورک اجرا شوند. در Android برای این کار از coroutineها در ViewModel و در TCA از Effect.run استفاده می‌شود.

دومین اشتباه — Intentهای بیش از حد جزئی. هر فشار کلید، حرکت اسلایدر و تغییر متن یک Intent جداگانه ایجاد می‌کند. برای فیلدهای ورودی این کار اضافی است — در چنین مواردی استفاده از Binding با جریان یک‌طرفه در داخل فرم (وضعیت محلی) قابل قبول است و Intent سراسری فقط برای اقدامات مهم (ارسال، ناوبری) ارسال می‌شود.

سومین اشتباه — عدم مدیریت لغو اثرات. اگر کاربر صفحه را ترک کند و coroutine یا Task به اجرا ادامه دهد، نتیجه ممکن است به Viewی که قبلاً نابود شده اعمال شود. در Android از viewModelScope.cancel() یا takeWhileActive() استفاده کنید؛ در TCA اثرات هنگام نابودی Store به طور خودکار لغو می‌شوند. طبق Google Issue Tracker (2024)، نشت‌های ناشی از coroutineهای ناتمام در بین ۵ علت اصلی crashes برنامه‌های 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 برای صفحات با یک فیلد ورودی بدون اعتبارسنجی، صفحات ایستا و صفحات placeholder اضافی است. اگر صفحه منطق تجاری ندارد و وضعیت آن به اقدامات کاربر وابسته نیست، UDF کد اضافی بدون سود اضافه می‌کند. برای چنین سناریوهایی اتصال یک‌طرفه ساده یا @State در SwiftUI کافی است. UDF زمانی توجیه‌پذیر است که تعداد وضعیت‌های ممکن صفحه از ۳–۴ بیشتر باشد و/یا عوارض جانبی وجود داشته باشد.

چگونه UDF را تست کنیم؟

از آنجا که Reducer یک تابع خالص است، تست آن به فراخوانی با ترکیب‌های مختلف State و Intent و بررسی State و Effect حاصل خلاصه می‌شود. در Android از Turbine برای تست StateFlow استفاده کنید: Intent ارسال کنید، انتشار بعدی State را بررسی کنید. در TCA یک TestStore داخلی وجود دارد که به طور خودکار بررسی می‌کند پس از Action فقط فیلدهای مورد انتظار State تغییر کرده و فقط Effectهای مورد انتظار اجرا شده‌اند.

آیا می‌توان UDF و Two-Way Binding را ترکیب کرد؟

بله، ترکیب مجاز است و اغلب بهینه است. برای فیلدهای ورودی داخل فرم از Two-Way Binding محلی (یا Binding در SwiftUI) استفاده کنید تا برای هر فشار کلید Intent ایجاد نشود. هنگام ارسال فرم، یک Intent با داده‌های جمع‌آوری‌شده ارسال کنید که توسط Reducer پردازش می‌شود. این رویکرد هیبریدی — UDF سراسری با Two-Way Binding محلی — در ۷۰٪ از برنامه‌های تجاری SwiftUI استفاده می‌شود (داده‌های Swift Community Survey 2024).

UDF، Redux و Elm چه وجه مشترکی دارند؟

هر سه الگو جریان داده یک‌طرفه با منبع واحد حقیقت را پیاده‌سازی می‌کنند. Elm (2012) — یک زبان تابعی که برای اولین بار چرخه خالص Model → View → Update را معرفی کرد. Redux (2015) Elm را برای JavaScript با مفهوم Store، Reducer و Action تطبیق داد. UDF تعمیم این ایده‌ها برای توسعه موبایل است. هر سه رویکرد پیش‌بینی‌پذیری تغییرات را از طریق به‌روزرسانی‌های اتمی (تجزیه‌ناپذیر) وضعیت تضمین می‌کنند.

خلاصه

  • Unidirectional Data Flow (UDF) — الگویی با چرخه یک‌طرفه State → View → Intent → Reducer که پیش‌بینی‌پذیری تغییرات وضعیت را تضمین می‌کند.
  • در Android UDF از طریق ViewModel + StateFlow + sealed class Intent و در iOS از طریق TCA (Reducer + Store) یا Observable بومی پیاده‌سازی می‌شود.
  • Google از سال ۲۰۲۳ UDF را به عنوان معماری اصلی برای Jetpack Compose توصیه می‌کند.
  • Reducer — یک تابع خالص بدون عوارض جانبی؛ تمام درخواست‌های شبکه و عملیات پایگاه داده به لایه Effect منتقل می‌شوند.
  • UDF مشکل حلقه‌های بی‌نهایت Two-Way Binding را با پردازش صریح Intent و Single Source of Truth برطرف می‌کند.
  • ریسک‌های اصلی — عوارض جانبی در Reducer، Intentهای بیش از حد جزئی و coroutineهای ناتمام.
  • رویکرد هیبریدی (Two-Way Binding محلی در فرم + UDF سراسری) برای اکثر برنامه‌های تجاری بهینه است.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید