MVI (Model-View-Intent) একটি রিঅ্যাকটিভ আর্কিটেকচারাল প্যাটার্ন যা ইউনিডিরেকশনাল ডেটা ফ্লো এবং অপরিবর্তনীয় অবস্থার উপর ভিত্তি করে। MVVM-এর বিপরীতে, যেখানে একটি ViewModel-এর একাধিক StateFlows থাকতে পারে, MVI একটি একক অবস্থা (State), অপরিবর্তনীয় অভিপ্রায় (Intent) এবং একটি বিশুদ্ধ রিডিউসার ফাংশন (Reducer) সংজ্ঞায়িত করে। MVI যেকোনো সময় স্ক্রিনের অবস্থার পূর্বাভাসযোগ্যতা নিশ্চিত করে। প্যাটার্নটি Mosby এবং Orbit লাইব্রেরি দ্বারা Android কমিউনিটিতে জনপ্রিয় হয়েছে। আরও জানতে Arkadii Ivanov-এর MVIKotlin দেখুন।
মূল বিষয়
MVI (Model-View-Intent) একটি রিঅ্যাকটিভ আর্কিটেকচারাল প্যাটার্ন যা Redux এবং Cycle.js-এর নীতির উপর নির্মিত। Model হল অপরিবর্তনীয় স্ক্রিন অবস্থা, Intent হল ব্যবহারকারী বা সিস্টেমের অভিপ্রায়, View অবস্থার সাবস্ক্রাইব করে এবং Intents পাঠায়। ডেটা একটি চক্রে প্রবাহিত হয়: ব্যবহারকারী View-এর সাথে ইন্টারঅ্যাক্ট করে → View একটি Intent তৈরি করে → Intent Reducer দ্বারা প্রক্রিয়াজাত হয় → Reducer একটি নতুন অবস্থা তৈরি করে → View নতুন অবস্থা গ্রহণ করে এবং পুনরায় রেন্ডার করে।
MVI এবং MVVM-এর মধ্যে প্রধান পার্থক্য হল একক সত্যের উৎস (Single Source of Truth)। MVVM-এ, একটি ViewModel-এর একাধিক LiveData/StateFlow (userState, loadingState, errorState) থাকতে পারে, যা অসামঞ্জস্যের দিকে নিয়ে যায়: loading=true এবং user=null একসাথে। MVI-তে, ঠিক একটি sealed class/interface State রয়েছে যা সম্পূর্ণ স্ক্রিন অবস্থা বর্ণনা করে। যেকোনো সময়, স্ক্রিনের অবস্থা অনন্যভাবে নির্ধারিত — loading=true থাকা অসম্ভব যখন ডেটা ইতিমধ্যে লোড হয়েছে। IT Sectr-এ, আমরা জটিল লজিকযুক্ত স্ক্রিন — অর্ডার ফর্ম, মাল্টি-স্টেপ রেজিস্ট্রেশন, আর্থিক স্ক্রিন — এর জন্য MVI প্রয়োগ করি যেখানে অবস্থার পূর্বাভাসযোগ্যতা গুরুত্বপূর্ণ।
| উপাদান | MVI-তে ভূমিকা | উদাহরণ |
|---|---|---|
| Intent | ব্যবহারকারী বা সিস্টেমের অভিপ্রায় | LoadUser, Refresh, SubmitForm |
| State | অপরিবর্তনীয় স্ক্রিন অবস্থা | sealed class UserState |
| Reducer | বিশুদ্ধ ফাংশন: State + Intent → State | fun reduce(state, intent) -> state |
| Middleware | সাইড ইফেক্ট পরিচালনা | নেটওয়ার্ক অনুরোধ, DB লেখা |
MVI চক্র পাঁচটি ধাপ নিয়ে গঠিত: 1) View একটি Intent পাঠায় (যেমন, LoadUser(42)); 2) Middleware (EffectHandler) একটি সাইড ইফেক্ট সম্পাদন করে — একটি নেটওয়ার্ক অনুরোধ; 3) ফলাফল সিস্টেমে একটি নতুন Intent হিসাবে ফেরত আসে; 4) Reducer বর্তমান অবস্থা এবং Intent নেয়, একটি নতুন অবস্থা তৈরি করে; 5) View নতুন অবস্থা গ্রহণ করে এবং পুনরায় রেন্ডার করে। প্রতিটি ধাপ পূর্বাভাসযোগ্য এবং পৃথকভাবে পরীক্ষাযোগ্য।
Android-এ MVI Intent এবং State-এর জন্য sealed classes, MVI লজিক সহ ViewModel এবং রিঅ্যাকটিভ রেন্ডারিংয়ের জন্য Jetpack Compose ব্যবহার করে বাস্তবায়িত হয়। ViewModel View থেকে Intent গ্রহণ করে, সাইড ইফেক্ট Middleware-কে অর্পণ করে, Reducer চালায় এবং StateFlow-এর মাধ্যমে নতুন অবস্থা প্রকাশ করে। Jetpack Compose অবস্থা পরিবর্তিত হলে UI পুনরায় রেন্ডার করে — MVI চক্রের জন্য আদর্শ।
// Intent — ব্যবহারকারীর অভিপ্রায়
sealed interface UserIntent {
data class LoadUser(val userId: Int) : UserIntent
data object Refresh : UserIntent
}
// State — একক স্ক্রিন অবস্থা
sealed interface UserState {
data object Idle : UserState
data object Loading : UserState
data class Success(val user: User) : UserState
data class Error(val message: String) : UserState
}
// Reducer — বিশুদ্ধ ফাংশন
object UserReducer {
fun reduce(state: UserState, intent: UserIntent): UserState = when (intent) {
is UserIntent.LoadUser -> UserState.Loading
is UserIntent.Refresh -> UserState.Loading
}
}
// MVI-সহ ViewModel
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UserState>(UserState.Idle)
val state: StateFlow<UserState> = _state.asStateFlow()
fun process(intent: UserIntent) {
val newState = UserReducer.reduce(_state.value, intent)
_state.value = newState
when (intent) {
is UserIntent.LoadUser -> loadUser(intent.userId)
is UserIntent.Refresh -> loadUser(/* পূর্ববর্তী ID */)
}
}
private fun loadUser(userId: Int) {
viewModelScope.launch {
repository.getUser(userId)
.onSuccess { user ->
_state.value = UserState.Success(user)
}
.onFailure { e ->
_state.value = UserState.Error(e.message ?: "Error")
}
}
}
}
// View Intent পাঠায়
fun UserScreen(viewModel: UserViewModel) {
val state by viewModel.state.collectAsState()
LaunchedEffect(Unit) { viewModel.process(UserIntent.LoadUser(42)) }
when (state) {
is UserState.Loading -> CircularProgressIndicator()
is UserState.Success -> UserCard((state as UserState.Success).user)
is UserState.Error -> ErrorView((state as UserState.Error).message)
is UserState.Idle -> Text("Press to load")
}
}
Middleware এবং সাইড ইফেক্ট — MVI-তে, একটি বিশুদ্ধ Reducer নেটওয়ার্ক অনুরোধ করতে পারে না। Middleware (EffectHandler বা Bootstrapper নামেও পরিচিত) Intent প্রক্রিয়া করে, সাইড ইফেক্ট সম্পাদন করে এবং চক্রে ফিরে একটি নতুন Intent নির্গত করে। Orbit MVI এবং MVIKotlin লাইব্রেরি পরীক্ষাযোগ্য ইফেক্ট সহ বিল্ট-ইন Middleware সমর্থন প্রদান করে। Middleware ছাড়া, MVI অতিরিক্ত Intent এবং State কাঠামো সহ MVVM-এ পরিণত হয়।
Arkadii Ivanov-এর MVIKotlin Kotlin Multiplatform-এর জন্য সবচেয়ে জনপ্রিয় MVI লাইব্রেরি। এটি Android, iOS, ওয়েব এবং JVM সমর্থন করে। এটি উপাদান প্রদান করে: Store (ViewModel), Bootstrapper (প্রাথমিক ইফেক্ট), Reducer, Middleware। অক্টোবর 2025 পর্যন্ত, লাইব্রেরিটি GitHub-এ 2.5K স্টার সংগ্রহ করেছে এবং বাণিজ্যিক প্রকল্পে ব্যবহৃত হচ্ছে, যার মধ্যে বড় রুশ ব্যাংকের অ্যাপ অন্তর্ভুক্ত। IT Sectr-এ, আমরা শেয়ার্ড বিজনেস লজিক সহ ক্রস-প্ল্যাটফর্ম KMP প্রকল্পের জন্য MVIKotlin ব্যবহার করি।
iOS-এ MVI Combine-ViewModel ছাড়া, Intent → State চক্রের মাধ্যমে বাস্তবায়িত হয়। View একটি ক্লোজারের মাধ্যমে Intent পাঠায়, Reducer একটি বিশুদ্ধ ফাংশন, এবং State অপরিবর্তনীয় ফিল্ড সহ একটি struct। State পরিবর্তিত হলে SwiftUI View পুনরায় রেন্ডার করে, যা অতিরিক্ত @Published প্রপার্টি ছাড়া MVI চক্রে পুরোপুরি ফিট করে। iOS-এ MVI বিশেষ করে SwiftUI ডেভেলপারদের মধ্যে জনপ্রিয় যারা Redux (JavaScript) থেকে স্থানান্তরিত হয়েছেন।
// State — অপরিবর্তনীয় কাঠামো
struct UserState: Equatable {
var user: User?
var isLoading = false
var errorMessage: String?
}
// Intent — অভিপ্রায় সহ enum
enum UserIntent {
case loadUser(id: Int)
case refresh
case userLoaded(User)
case loadFailed(Error)
}
// Reducer — বিশুদ্ধ ফাংশন
func userReducer(state: UserState, intent: UserIntent) -> UserState {
var newState = state
switch intent {
case .loadUser, .refresh:
newState.isLoading = true
newState.errorMessage = nil
case .userLoaded(let user):
newState.isLoading = false
newState.user = user
case .loadFailed(let error):
newState.isLoading = false
newState.errorMessage = error.localizedDescription
}
return newState
}
// Store — অবস্থার মালিক এবং ইফেক্ট পরিচালনা করে
final class UserStore: ObservableObject {
@Published private(set) var state = UserState()
private let service: UserService
init(service: UserService) {
self.service = service
}
func dispatch(_ intent: UserIntent) {
// 1. Reducer অবস্থা আপডেট করে
state = userReducer(state: state, intent: intent)
// 2. Side effects (যদি প্রয়োজন হয়)
switch intent {
case .loadUser(let id), .refresh:
service.fetchUser(id: id) { [weak self] result in
switch result {
case .success(let user):
self?.dispatch(.userLoaded(user))
case .failure(let error):
self?.dispatch(.loadFailed(error))
}
}
default: break
}
}
}
TCA (The Composable Architecture) Point-Free-এর iOS-এর জন্য সবচেয়ে জনপ্রিয় MVI বাস্তবায়ন, যা SwiftUI এবং Combine-এর উপর নির্মিত। TCA Store, Reducer, Effect এবং Environment প্রদান করে। অক্টোবর 2025 পর্যন্ত, GitHub-এ এর স্টার 13K-এর বেশি — এটি iOS-এ MVI-এর ডি ফ্যাক্টো স্ট্যান্ডার্ড। TCA Starbucks অ্যাপ, Airbnb (আংশিকভাবে) এবং অনেক ইন্ডি প্রকল্পে ব্যবহৃত হয়। কাস্টম MVI-এর বিপরীতে, TCA টেস্টিং, নেভিগেশন এবং সাইড ইফেক্ট আউট অফ দ্য বক্স সমাধান করে।
iOS-এ MVI বনাম MVVM — TCA/MVI অবস্থার পূর্বাভাসযোগ্যতা প্রদান করে কিন্তু আরও বয়লারপ্লেট কোড (Reducer, State, Action) প্রয়োজন। @Published-সহ MVVM সাধারণ স্ক্রিনের জন্য সহজ। IT Sectr-এ, আমরা 80% স্ক্রিনের জন্য MVVM এবং 20% জটিল স্ক্রিন — আর্থিক লেনদেন, মাল্টি-স্টেপ ফর্ম, ড্র্যাগ-এন্ড-ড্রপ ইন্টারফেস — এর জন্য MVI (TCA) ব্যবহার করি, যেখানে অবস্থার ত্রুটি ব্যবহারকারীর অর্থ খরচ করতে পারে।
MVI এবং MVVM একই সমস্যা সমাধান করে — প্রেজেন্টেশন লেয়ার সংগঠিত করা — কিন্তু অবস্থা পরিচালনার ভিন্ন পদ্ধতির সাথে। MVVM একাধিক রিঅ্যাকটিভ উৎস (LiveData, @Published) অনুমতি দেয়, যা অসামঞ্জস্যের কারণ হতে পারে। MVI যেকোনো সময় ঠিক একটি অবস্থার নিশ্চয়তা দেয়, এটিকে আরও কঠোর এবং পূর্বাভাসযোগ্য করে তোলে, কিন্তু কোডের পরিমাণ বাড়িয়ে দেয়।
| মানদণ্ড | MVVM | MVI |
|---|---|---|
| অবস্থা | একাধিক LiveData/StateFlow | একক sealed class State |
| ডেটা প্রবাহ | দ্বিমুখী (View → ViewModel, LiveData → View) | একমুখী (Intent → Reducer → State → View) |
| সাইড ইফেক্ট | সরাসরি ViewModel-এ | Middleware/EffectHandler-এর মাধ্যমে |
| পরীক্ষা | ViewModel-এর ইউনিট টেস্ট | Reducer + Middleware-এর ইউনিট টেস্ট |
| বয়লারপ্লেট কোড | সর্বনিম্ন | Reducer + State + Intent + Middleware |
কখন MVI বেছে নেবেন — যেসব স্ক্রিনে অবস্থা কঠোরভাবে নির্ধারক হতে হবে: আর্থিক কার্যক্রম, শপিং কার্ট, প্রতিটি ধাপে বৈধতা সহ মাল্টি-স্টেপ ফর্ম। এই পরিস্থিতিতে, অবস্থার ত্রুটির খরচ (যেমন, দুটি LiveData-এর মধ্যে রেস কন্ডিশনের কারণে একটি আইটেম অনুপস্থিত কার্টের মোট দেখানো) অতিরিক্ত কোডের খরচের চেয়ে বেশি। MVVM-এ, আপনি দলের শৃঙ্খলার উপর নির্ভর করেন; MVI-তে, আপনি আর্কিটেকচারের উপর নির্ভর করেন।
কখন MVVM যথেষ্ট — 80% স্ট্যান্ডার্ড স্ক্রিন: ব্যবহারকারী তালিকা, প্রোফাইল, সেটিংস, সংবাদ ফিড। এখানে, একটি একক অবস্থা অতিরিক্ত, এবং অতিরিক্ত MVI কাঠামো উন্নয়ন ধীর করবে। IT Sectr-এ, নিয়ম হল: যদি একটি স্ক্রিনে ট্রানজিশন (লোডিং → ডেটা → ত্রুটি → পুনরায় চেষ্টা → লোডিং → ডেটা) সহ 3+ সম্ভাব্য অবস্থা থাকে — MVI ব্যবহার করুন। যদি একটি স্ক্রিনে 1-2 অ্যাসিঙ্ক্রোনাস অপারেশন থাকে — MVVM ব্যবহার করুন।
Sealed State — সেরা অনুশীলন MVI-তে। অবস্থাটি ভেরিয়েন্ট সহ একটি sealed class/interface হিসাবে সংজ্ঞায়িত: Loading, Success(data), Error(message)। এটি নিশ্চিত করে যে View একটি অসামঞ্জস্যপূর্ণ অবস্থায় শেষ হবে না — loading=true হলে ডেটা প্রদর্শন করা যায় না কারণ Loading এবং Success ভিন্ন ক্লাস। অবস্থার সাথে সম্পর্কিত সমস্ত ডেটা sealed ভেরিয়েন্টের ভিতরে থাকে: Success-এ ব্যবহারকারী থাকে, Error-এ ত্রুটির বার্তা থাকে।
Reducer-কে বিশুদ্ধ ফাংশন থাকতে হবে — API, DB বা SharedPreferences কল ছাড়া। একটি বিশুদ্ধ ফাংশন State এবং Intent নেয় এবং State ফেরত দেয়। সাইড ইফেক্ট (নেটওয়ার্ক, DB, নেভিগেশন, টোস্ট) Middleware-এ বা Reducer কল করার পর Store.dispatch-এ পরিচালিত হয়। যদি Reducer সাইড ইফেক্ট দ্বারা দূষিত হয়, MVI পরীক্ষাযোগ্যতা এবং পূর্বাভাসযোগ্যতা হারায় — আপনি সুবিধা ছাড়াই অতিরিক্ত কাঠামো সহ MVVM পান।
সাধারণ ভুল — sealed class-এর পরিবর্তে nullable ফিল্ড সহ data class হিসাবে State ঘোষণা করা: data class UserState(val user: User?, val isLoading: Boolean, val error: String?)। এটি MVVM-এর সমতুল্য, MVI নয় — View-কে বৈধতার জন্য ফিল্ড কম্বিনেশন পরীক্ষা করতে হবে। sealed পদ্ধতিতে, অবৈধ কম্বিনেশন (isLoading=true এবং user!=null) টাইপ স্তরে অসম্ভব। দ্বিতীয় ভুল হল Middleware-এ বিজনেস লজিক রাখার পরিবর্তে Intent-এ (Intent.LoadUserBeforeXHours) বিজনেস লজিক রাখা।
সচরাচর জিজ্ঞাসিত প্রশ্ন
MVI একটি একক অপরিবর্তনীয় sealed State ক্লাস এবং Reducer-এর মাধ্যমে একমুখী ডেটা প্রবাহ ব্যবহার করে। MVVM দ্বিমুখী বাইন্ডিং সহ একাধিক LiveData/StateFlow অনুমতি দেয়। MVI টাইপ স্তরে অবস্থার ধারাবাহিকতা নিশ্চিত করে — loading=true এবং user=null একসাথে থাকা অসম্ভব। MVVM ডেভেলপারের শৃঙ্খলার উপর নির্ভর করে।
প্রধান: MVIKotlin (Arkadii Ivanov, 2.5K স্টার, Kotlin Multiplatform), Orbit MVI (BabyJ, 1.3K স্টার), Mobius (Spotify, Kotlin/Java)। MVIKotlin Kotlin-এর জন্য সবচেয়ে জনপ্রিয়, Orbit শেখার জন্য সবচেয়ে সহজ। তিনটিই পরীক্ষাযোগ্য Reducer এবং Middleware সমর্থন করে। Jetpack Compose-এর জন্য, আপনি sealed State + Reducer ব্যবহার করে লাইব্রেরি ছাড়া সাধারণ MVI লিখতে পারেন।
না — sealed Intent + sealed State + ViewModel + StateFlow নির্ভরতা ছাড়া কাজ করা MVI দেয়। লাইব্রেরি (MVIKotlin, Orbit, TCA) Middleware, সাইড ইফেক্ট টেস্টিং এবং DI ইন্টিগ্রেশন যোগ করে। সাধারণ প্রকল্পের জন্য, লাইব্রেরির ওজন অযৌক্তিক। 20+ স্ক্রিনযুক্ত জটিল প্রকল্পের জন্য, লাইব্রেরি কাঠামোবদ্ধ ইফেক্ট পরিচালনার সাথে মূল্য পরিশোধ করে।
MVI TCA (The Composable Architecture) — SwiftUI কমিউনিটির সবচেয়ে জনপ্রিয় আর্কিটেকচার — এর মাধ্যমে iOS-এর জন্য দুর্দান্ত কাজ করে। TCA মূলত MVI + Redux + Combine। iOS-এ, আপনি ObservableObject এবং একটি বিশুদ্ধ reducer ফাংশন ব্যবহার করে TCA ছাড়া MVI বাস্তবায়ন করতে পারেন। অপরিবর্তনীয় State-সহ SwiftUI MVI চক্রে পুরোপুরি ফিট করে।
Reducer একটি বিশুদ্ধ ফাংশন হিসাবে ইউনিট টেস্ট দিয়ে পরীক্ষা করা হয়: প্রাথমিক State সেট করুন, Intent পাঠান, ফলস্বরূপ State পরীক্ষা করুন। Middleware একটি মক রিপোজিটরি দিয়ে পরীক্ষা করা হয়: LoadUser-এর পরে getUser কল করা হয়েছিল কিনা যাচাই করুন। ViewModel পরীক্ষা: Intent পাঠান, StateFlow পরীক্ষা করুন। MVI MVVM-এর চেয়ে পরীক্ষা করা সহজ কারণ Reducer লুকানো নির্ভরতা ছাড়া একটি বিশুদ্ধ ফাংশন।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন