ইউনিডিরেকশনাল ডেটা ফ্লো কী তা বুঝুন — একটি একমুখী ডেটা প্রবাহ, একটি আর্কিটেকচারাল প্যাটার্ন যেখানে ডেটা একটি বন্ধ লুপ State → View → Intent → Reducer → State-এ ফিডব্যাক লুপ ছাড়া চলে। দ্বিমুখী বাইন্ডিংয়ের বিপরীতে, UDF গ্যারান্টি দেয় যে অবস্থার পরিবর্তন শুধুমাত্র স্পষ্ট কর্মের (Intent/Event) মাধ্যমে ঘটে, যা ডেটা প্রবাহকে পূর্বাভাসযোগ্য এবং ট্রেসযোগ্য করে তোলে। Google I/O 2024 অনুসারে, UDF মাঝারি এবং উচ্চ জটিলতার বিজনেস লজিকযুক্ত Jetpack Compose এবং SwiftUI অ্যাপ্লিকেশনের জন্য প্রস্তাবিত আর্কিটেকচার।
মূল বিষয়
ইউনিডিরেকশনাল ডেটা ফ্লো (UDF) একটি আর্কিটেকচারাল প্যাটার্ন যেখানে ডেটা একটি বন্ধ লুপে এক দিকে চলে, যা View এবং Model-এর মধ্যে ফিডব্যাক লুপ দূর করে। দ্বিমুখী বাইন্ডিংয়ের বিপরীতে, যেখানে UI-তে পরিবর্তন তাৎক্ষণিকভাবে মডেল আপডেট করে, UDF-এর প্রতিটি অবস্থা পরিবর্তনের জন্য একটি স্পষ্ট কর্মের (Intent, Event, Action) প্রয়োজন হয়। এটি ডেটা প্রবাহকে সম্পূর্ণ পূর্বাভাসযোগ্য করে তোলে: যেকোনো সময় নির্ধারণ করা যায় কোন কর্মটি বর্তমান অবস্থার দিকে নিয়ে গেছে।
UDF ধারণাটি ওয়েব ফ্রেমওয়ার্ক — Redux (JavaScript, 2015) এবং Elm (2012) — থেকে এসেছে এবং মোবাইল ডেভেলপমেন্টের জন্য অভিযোজিত হয়েছে। Google I/O 2024 অনুসারে, UDF Jetpack Compose-এর জন্য প্রস্তাবিত আর্কিটেকচার হয়ে উঠেছে, যা LiveData-সহ ক্লাসিক MVVM-কে প্রতিস্থাপন করছে। iOS-এ, Point-Free-এর The Composable Architecture (TCA)-তে একই ধরনের পদ্ধতি বাস্তবায়িত হয়েছে, যা Swift Community Survey (2024) অনুসারে 15% এরও বেশি iOS ডেভেলপার ব্যবহার করেন।
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-এ নেটওয়ার্ক কল বা ডেটাবেস লেখা থাকে, তাহলে ডেটা প্রবাহ পরীক্ষা এবং ডিবাগ করা অসম্ভব হয়ে পড়ে। Reducer কল করার আগে সমস্ত পার্শ্বপ্রতিক্রিয়া ViewModel করুটিন বা Swift Task-এ কার্যকর করতে হবে, এবং ফলাফলটি একটি নতুন Intent হিসেবে পাঠাতে হবে।
Android-এ, UDF বাস্তবায়ন তিনটি Jetpack উপাদানের উপর নির্মিত: ViewModel জীবনচক্র পরিচালনা করে, StateFlow একটি প্রতিক্রিয়াশীল অবস্থা স্ট্রিম প্রদান করে, Intent (sealed class) সমস্ত সম্ভাব্য ব্যবহারকারী কর্ম বর্ণনা করে। View Compose-এ collectAsState() বা View সিস্টেমে observe()-এর মাধ্যমে 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) }
)
}উদাহরণটি Android-এ সম্পূর্ণ UDF চক্র প্রদর্শন করে: LoginIntent সমস্ত সম্ভাব্য কর্ম (ইমেল পরিবর্তন, পাসওয়ার্ড পরিবর্তন, ফর্ম জমা) বর্ণনা করে, LoginState অপরিবর্তনীয় অবস্থা, LoginViewModel Intents প্রক্রিয়াজাত করে এবং StateFlow আপডেট করে, এবং Compose স্ক্রিন collectAsState()-এর মাধ্যমে অবস্থায় সাবস্ক্রাইব করে। প্রতিটি অবস্থা পরিবর্তন একটি নির্দিষ্ট Intent প্রক্রিয়াজাতকরণের ফলাফল, যা ডেটা প্রবাহকে সম্পূর্ণ স্বচ্ছ করে তোলে।
iOS-এ, UDF বাস্তবায়িত হয় The Composable Architecture (TCA) Point-Free বা iOS 17+ এর সাথে নেটিভ Observable প্যাটার্নের মাধ্যমে। 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()-এর মাধ্যমে Actions পাঠায়। Store ধ্বংস হলে TCA স্বয়ংক্রিয়ভাবে প্রভাব বাতিল করে, মেমরি লিক প্রতিরোধ করে।
MVVM এবং UDF প্রায়ই বিভ্রান্ত হয়, তবে তাদের মধ্যে মৌলিক পার্থক্য রয়েছে। MVVM একটি কাঠামোগত প্যাটার্ন যা কোডকে তিনটি স্তরে (Model, View, ViewModel) বিভক্ত করে কিন্তু ডেটা প্রবাহের দিক নির্ধারণ করে না। UDF একটি আচরণগত প্যাটার্ন যা বর্ণনা করে কীভাবে ডেটা সেই কাঠামোর মধ্যে চলে। LiveData-সহ MVVM-এ, দ্বিমুখী বাইন্ডিং এবং একমুখী প্রবাহ উভয়ই সম্ভব — UDF MVVM-এ কঠোর Intent প্রক্রিয়াজাতকরণ নিয়ম যোগ করে।
Android Developers ডকুমেন্টেশন (2024) অনুসারে, Compose-এর জন্য প্রস্তাবিত আর্কিটেকচার হল MVVM-এর ভিতরে UDF: ViewModel State সংরক্ষণ করে এবং Intents প্রক্রিয়াজাত করে, View State-এ সাবস্ক্রাইব করে এবং Intents পাঠায়। Google ব্যবসায়িক লজিক ছাড়া সাধারণ স্ক্রিনের জন্য শুধুমাত্র DataBinding-এর মাধ্যমে দ্বিমুখী বাইন্ডিং-সহ ক্লাসিক MVVM সুপারিশ করে। 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 লাভ ছাড়া অপ্রয়োজনীয় কোড যোগ করে। এই ধরনের পরিস্থিতির জন্য, একমুখী বাইন্ডিং বা SwiftUI-তে সরল @State যথেষ্ট। UDF ন্যায্য যখন সম্ভাব্য স্ক্রিন অবস্থার সংখ্যা ৩-৪-এর বেশি হয় এবং/অথবা পার্শ্বপ্রতিক্রিয়া বিদ্যমান থাকে।
যেহেতু Reducer একটি বিশুদ্ধ ফাংশন, এটি পরীক্ষা করা State এবং Intent-এর বিভিন্ন সংমিশ্রণে কল করা এবং ফলস্বরূপ State এবং Effect যাচাই করার মধ্যে সীমাবদ্ধ। Android-এ, StateFlow পরীক্ষার জন্য Turbine ব্যবহার করুন: একটি Intent পাঠান, পরবর্তী State নির্গমন পরীক্ষা করুন। TCA-তে, একটি অন্তর্নির্মিত TestStore রয়েছে যা স্বয়ংক্রিয়ভাবে যাচাই করে যে Action-এর পরে শুধুমাত্র প্রত্যাশিত State ফিল্ড পরিবর্তিত হয়েছে এবং শুধুমাত্র প্রত্যাশিত Effects কার্যকর হয়েছে।
হ্যাঁ, এগুলিকে একত্রিত করা গ্রহণযোগ্য এবং প্রায়শই সর্বোত্তম। ফর্মের ভিতরে ইনপুট ফিল্ডের জন্য, প্রতিটি কীস্ট্রোকে Intent তৈরি এড়াতে স্থানীয় দ্বিমুখী বাইন্ডিং (বা SwiftUI-তে Binding) ব্যবহার করুন। ফর্ম জমা দেওয়ার সময়, সংগৃহীত ডেটা-সহ একটি একক Intent পাঠান, যা Reducer দ্বারা প্রক্রিয়াজাত হয়। এই হাইব্রিড পদ্ধতি — স্থানীয় দ্বিমুখী বাইন্ডিং-সহ বৈশ্বিক UDF — 70% বাণিজ্যিক SwiftUI অ্যাপ্লিকেশনে ব্যবহৃত হয় (Swift Community Survey 2024 তথ্য)।
তিনটি প্যাটার্নই একক সত্যের উৎস-সহ একমুখী ডেটা প্রবাহ বাস্তবায়ন করে। Elm (2012) — একটি কার্যকরী ভাষা — প্রথমবারের মতো বিশুদ্ধ Model → View → Update চক্র চালু করেছিল। Redux (2015) Store, Reducer এবং Action-এর ধারণা-সহ JavaScript-এর জন্য Elm-কে অভিযোজিত করেছিল। UDF হল মোবাইল ডেভেলপমেন্টের জন্য এই ধারণাগুলির একটি সাধারণীকরণ। তিনটি পদ্ধতিই পারমাণবিক অবস্থা আপডেটের মাধ্যমে পরিবর্তনের পূর্বাভাসযোগ্যতা নিশ্চিত করে।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন