بدانید Unidirectional Data Flow چیست — جریان داده یکطرفه، یک الگوی معماری که در آن دادهها در یک چرخه بسته State → View → Intent → Reducer → State بدون بازخورد حرکت میکنند. برخلاف اتصال دوطرفه، UDF تضمین میکند که تغییر وضعیت فقط از طریق اقدامات صریح (Intent/Event) رخ میدهد، که جریان داده را قابل پیشبینی و قابل ردیابی میکند. طبق Google I/O 2024، UDF معماری توصیهشده برای برنامههای Jetpack Compose و SwiftUI با منطق تجاری متوسط و پیچیده است.
نکات اصلی
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 (نیت) تبدیل میشود؛ 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 جدید ارسال شود.
در Android پیادهسازی UDF بر سه مؤلفه Jetpack استوار است: ViewModel چرخه حیات را مدیریت میکند، StateFlow جریان واکنشی وضعیتها را فراهم میکند، Intent (sealed class) تمام اقدامات ممکن کاربر را توصیف میکند. View از طریق collectAsState() در Compose یا observe() در سیستم View در StateFlow مشترک میشود.
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 خاص است که جریان داده را کاملاً شفاف میکند.
در iOS UDF از طریق The Composable Architecture (TCA) از Point-Free یا الگوی Observable بومی از iOS 17+ پیادهسازی میشود. TCA یک چرخه آماده State + Action + Reducer + Store ارائه میدهد که در آن Store تنها منبع حقیقت است و View از طریق @Observable یا ObservableObject در تغییرات مشترک میشود.
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 مدیریت میکند و از نشت حافظه جلوگیری میکند.
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 |
رایجترین اشتباه — عوارض جانبی در داخل 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 قرار دارند.
سوالات متداول
MVI (Model-View-Intent) — یک مورد خاص از UDF با سه عنصر اجباری است: Intent (نیت)، Model (وضعیت)، View (نمایش). تفاوت اصلی این است که در MVI هر وضعیت صفحه با یک ساختار تغییرناپذیر (Sealed class) توصیف میشود و View یک تابع خالص از Model به UI است. UDF یک اصطلاح گستردهتر است که هر جریان یکطرفه از جمله Redux و Elm را توصیف میکند. در مستندات Google از اصطلاح UDF به عنوان نام عمومی و از MVI به عنوان پیادهسازی خاص استفاده میشود.
UDF برای صفحات با یک فیلد ورودی بدون اعتبارسنجی، صفحات ایستا و صفحات placeholder اضافی است. اگر صفحه منطق تجاری ندارد و وضعیت آن به اقدامات کاربر وابسته نیست، UDF کد اضافی بدون سود اضافه میکند. برای چنین سناریوهایی اتصال یکطرفه ساده یا @State در SwiftUI کافی است. UDF زمانی توجیهپذیر است که تعداد وضعیتهای ممکن صفحه از ۳–۴ بیشتر باشد و/یا عوارض جانبی وجود داشته باشد.
از آنجا که Reducer یک تابع خالص است، تست آن به فراخوانی با ترکیبهای مختلف State و Intent و بررسی State و Effect حاصل خلاصه میشود. در Android از Turbine برای تست StateFlow استفاده کنید: Intent ارسال کنید، انتشار بعدی State را بررسی کنید. در TCA یک TestStore داخلی وجود دارد که به طور خودکار بررسی میکند پس از Action فقط فیلدهای مورد انتظار State تغییر کرده و فقط Effectهای مورد انتظار اجرا شدهاند.
بله، ترکیب مجاز است و اغلب بهینه است. برای فیلدهای ورودی داخل فرم از Two-Way Binding محلی (یا Binding در SwiftUI) استفاده کنید تا برای هر فشار کلید Intent ایجاد نشود. هنگام ارسال فرم، یک Intent با دادههای جمعآوریشده ارسال کنید که توسط Reducer پردازش میشود. این رویکرد هیبریدی — UDF سراسری با Two-Way Binding محلی — در ۷۰٪ از برنامههای تجاری SwiftUI استفاده میشود (دادههای Swift Community Survey 2024).
هر سه الگو جریان داده یکطرفه با منبع واحد حقیقت را پیادهسازی میکنند. Elm (2012) — یک زبان تابعی که برای اولین بار چرخه خالص Model → View → Update را معرفی کرد. Redux (2015) Elm را برای JavaScript با مفهوم Store، Reducer و Action تطبیق داد. UDF تعمیم این ایدهها برای توسعه موبایل است. هر سه رویکرد پیشبینیپذیری تغییرات را از طریق بهروزرسانیهای اتمی (تجزیهناپذیر) وضعیت تضمین میکنند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید