افهم ما هو تدفق البيانات أحادي الاتجاه — تدفق البيانات في اتجاه واحد، نمط معماري حيث تتحرك البيانات في حلقة مغلقة State → View → Intent → Reducer → State بدون حلقات ردود فعل. على عكس الربط ثنائي الاتجاه، يضمن UDF أن تغييرات الحالة تحدث فقط من خلال إجراءات صريحة (Intent/Event)، مما يجعل تدفق البيانات قابلاً للتنبؤ والتتبع. وفقًا لـ Google I/O 2024، فإن UDF هي البنية الموصى بها لتطبيقات Jetpack Compose وSwiftUI ذات منطق الأعمال متوسط وعالي التعقيد.
النقاط الرئيسية
تدفق البيانات أحادي الاتجاه (UDF) هو نمط معماري حيث تتحرك البيانات في اتجاه واحد في حلقة مغلقة، مما يلغي حلقات ردود الفعل بين View وModel. على عكس الربط ثنائي الاتجاه، حيث يؤدي التغيير في واجهة المستخدم إلى تحديث النموذج فورًا، يتطلب 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، والذي يستخدمه أكثر من 15% من مطوري iOS وفقًا لاستطلاع مجتمع Swift (2024).
الميزة الرئيسية لـ UDF هي مصدر واحد للحقيقة (SSOT): يتم تخزين جميع حالة التطبيق في مكان واحد ويتم تعديلها من خلال عمليات محددة بدقة. هذا يبسط التصحيح والاختبار وإعادة إنتاج الأخطاء، حيث يتم تسجيل كل تغيير في الحالة ويمكن إعادة إنتاجه عن طريق إعادة إرسال نفس Intents.
تتكون دورة 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 يحتوي على استدعاء شبكة أو كتابة في قاعدة البيانات، يصبح اختبار وتصحيح تدفق البيانات مستحيلًا. يجب تنفيذ جميع الآثار الجانبية في كوروتين ViewModel أو Swift Task قبل استدعاء Reducer، ويجب إرسال النتيجة كـ Intent جديد.
في Android، يعتمد تنفيذ UDF على ثلاثة مكونات من Jetpack: ViewModel يدير دورة الحياة، StateFlow يوفر تدفق حالة تفاعلي، Intent (sealed class) يصف جميع الإجراءات الممكنة للمستخدم. تشترك View في StateFlow عبر collectAsState() في Compose أو observe() في نظام View.
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 محدد، مما يجعل تدفق البيانات شفافًا تمامًا.
في 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 في تغييرات Store عبر WithViewStore وترسل Actions عبر send(). تتعامل TCA تلقائيًا مع إلغاء التأثيرات عند تدمير Store، مما يمنع تسرب الذاكرة.
غالبًا ما يتم الخلط بين 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 |
الخطأ الأكثر شيوعًا هو الآثار الجانبية داخل Reducer. المطورون المعتادون على MVVM يضعون طلبات الشبكة مباشرة في معالج Intent، مما يجعل Reducer غير نقي ويكسر قابلية الاختبار. يجب إعادة جميع التأثيرات كقيمة (Effect / SideEffect) وتنفيذها بواسطة البنية التحتية للإطار. في Android، تُستخدم الكوروتينات في ViewModel لهذا؛ في TCA — Effect.run.
الخطأ الثاني هو Intents مفصلة بشكل مفرط. كل ضغطة مفتاح، وحركة شريط تمرير، وتغيير نص يولد Intent منفصل. لحقول الإدخال، هذا مفرط — في مثل هذه الحالات، من المقبول استخدام Binding مع تدفق أحادي الاتجاه داخل النموذج (حالة محلية)، وإرسال Intent عالمي فقط للإجراءات الهامة (إرسال، تنقل).
الخطأ الثالث هو عدم وجود معالجة لإلغاء التأثيرات. إذا غادر المستخدم الشاشة بينما لا يزال الكوروتين أو Task قيد التنفيذ، فقد يتم تطبيق النتيجة على View مدمرة بالفعل. في Android، استخدم viewModelScope.cancel() أو takeWhileActive()؛ في TCA، يتم إلغاء التأثيرات تلقائيًا عند تدمير Store. وفقًا لـ Google Issue Tracker (2024)، تسرب الكوروتينات غير المكتملة هو من بين الأسباب الخمسة الأولى للأعطال في تطبيقات Compose.
الأسئلة الشائعة
MVI (Model-View-Intent) هو حالة محددة من UDF مع ثلاثة عناصر إلزامية: Intent (نية)، Model (حالة)، View (عرض). الفرق الرئيسي هو أنه في MVI، يتم وصف كل حالة شاشة بهيكل واحد غير قابل للتغيير (Sealed class)، وView هي دالة نقية من Model إلى UI. UDF هو مصطلح أوسع يصف أي تدفق أحادي الاتجاه، بما في ذلك Redux وElm. في وثائق Google، يُستخدم مصطلح UDF كاسم عام، بينما MVI هو تنفيذ محدد.
UDF مفرط للشاشات التي تحتوي على حقل إدخال واحد بدون تحقق، والصفحات الثابتة، وشاشات العناصر النائبة. إذا لم يكن للشاشة منطق أعمال ولا تعتمد حالتها على إجراءات المستخدم، يضيف UDF كودًا غير ضروري بدون فائدة. لمثل هذه السيناريوهات، يكفي الربط أحادي الاتجاه أو @State بسيط في SwiftUI. يكون UDF مبررًا عندما يتجاوز عدد حالات الشاشة المحتملة 3–4 و/أو توجد آثار جانبية.
نظرًا لأن Reducer هي دالة نقية، فإن اختبارها يتلخص في استدعائها بمجموعات مختلفة من State وIntent والتحقق من State وEffect الناتجين. في Android، استخدم Turbine لاختبار StateFlow: أرسل Intent، وتحقق من انبعاث State التالي. في TCA، يوجد TestStore مدمج يتحقق تلقائيًا من أنه بعد Action، تغيرت فقط حقول State المتوقعة وتم تنفيذ Effects المتوقعة فقط.
نعم، الجمع بينهما مقبول وغالبًا ما يكون مثاليًا. لحقول الإدخال داخل النموذج، استخدم الربط ثنائي الاتجاه المحلي (أو Binding في SwiftUI) لتجنب إنشاء Intent عند كل ضغطة مفتاح. عند إرسال النموذج، أرسل Intent واحدًا مع البيانات المجمعة، والتي تتم معالجتها بواسطة Reducer. هذا النهج الهجين — UDF عالمي مع ربط ثنائي الاتجاه محلي — يُستخدم في 70% من تطبيقات SwiftUI التجارية (بيانات Swift Community Survey 2024).
جميع الأنماط الثلاثة تنفذ تدفق بيانات أحادي الاتجاه مع مصدر واحد للحقيقة. Elm (2012) — لغة وظيفية — قدمت لأول مرة الدورة النقية Model → View → Update. قام Redux (2015) بتكييف Elm لجافا سكريبت بمفاهيم Store وReducer وAction. UDF هو تعميم لهذه الأفكار لتطوير الأجهزة المحمولة. تضمن المناهج الثلاثة قابلية التنبؤ بالتغييرات من خلال تحديثات الحالة الذرية.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا