MVI ও ইউনিডাইরেকশনাল ডেটা ফ্লো
এই পাঠে যা শিখবেন
- MVI-এর তিনটি অংশ — Intent, reducer-সদৃশ ফাংশন, State, View — এবং কেন এটি Redux-এর আকৃতির সাথে মেলে
- একটি মোবাইল কাউন্টার স্ক্রিনের জন্য সত্যিকারের, ইমিউটেবল reduce ফাংশন লেখা
- চারটি Intent ট্রেস করে প্রতিবার State-এর id() পরিবর্তন যাচাই করে প্রকৃত ইমিউটেবিলিটি প্রমাণ করা
- MVI-এর ইমিউটেবিলিটি কীভাবে MVVM-এর সমতা-চেক-করা অপটিমাইজেশন থেকে আলাদা
১ · Intent → নতুন State → View — Redux-এর মতোই আকৃতি, মোবাইল নামকরণ
FSWF-এর Flux/Redux পাঠ শিখিয়েছে
কীভাবে একটি Action dispatch হয়ে একটি reducer-এর মাধ্যমে নতুন state তৈরি করে, আর View সেই নতুন state
অনুযায়ী রেন্ডার করে — কখনো সরাসরি state মিউটেট হয় না। মোবাইল আর্কিটেকচারে (বিশেষত আধুনিক Android
Jetpack Compose-ভিত্তিক অ্যাপে) একই ধারণাটি MVI নামে পরিচিত — Action-এর জায়গায় বলা হয়
Intent (ইউজারের "কী করতে চায়" তার বর্ণনা — Android-এর নেভিগেশন Intent
ক্লাসের সাথে নাম মিললেও এটি ভিন্ন একটি ধারণা)। চক্রটি হুবহু একই: এক দিকেই প্রবাহিত হয়, কখনো উল্টো দিকে নয়।
২ · একটি কাউন্টার স্ক্রিনের MVI — বাস্তব কোড
নিচের কোডে reduce() কখনো state-এর ভেতরের ডিকশনারি in-place পরিবর্তন করে না —
প্রতিবার {**state, ...} দিয়ে একটি নতুন ডিকশনারি তৈরি করে রিটার্ন করে। চারটি
Intent ক্রমান্বয়ে dispatch করে প্রতিবার id(old_state) != id(new_state) চেক করে যাচাই করা
হচ্ছে যে সত্যিই একটি নতুন অবজেক্ট তৈরি হয়েছে, পুরনোটা মিউটেট হয়নি।
# ---------- REDUCE -- Intent থেকে সবসময় একটি নতুন, ইমিউটেবল State তৈরি করে ----------
def reduce(state, intent):
intent_type = intent["type"]
if intent_type == "INCREMENT":
return {**state, "count": state["count"] + state["step"]}
elif intent_type == "DECREMENT":
return {**state, "count": state["count"] - state["step"]}
elif intent_type == "SET_STEP":
return {**state, "step": intent["value"]}
elif intent_type == "RESET":
return {**state, "count": 0}
return state # অচেনা Intent হলে state অপরিবর্তিত ফেরত (তবুও একই অবজেক্ট, নতুন নয়)
# ---------- STORE -- Intent গ্রহণ করে, reduce কল করে, বর্তমান State ধরে রাখে ----------
class CounterMVIStore:
def __init__(self, initial_state):
self.state = initial_state
def dispatch(self, intent):
old_state = self.state
new_state = reduce(self.state, intent)
self.state = new_state
return old_state, new_state
# ---------- VIEW -- বর্তমান State রেন্ডার করে ----------
def view(state):
print(f"[View] কাউন্ট: {state['count']} (step={state['step']})")
store = CounterMVIStore({"count": 0, "step": 1})
print("== প্রাথমিক State ==")
view(store.state)
intents = [
{"type": "INCREMENT"},
{"type": "INCREMENT"},
{"type": "DECREMENT"},
{"type": "SET_STEP", "value": 5},
]
for i, intent in enumerate(intents, start=1):
print(f"\n== Intent #{i}: {intent} ==")
old_state, new_state = store.dispatch(intent)
view(store.state)
print(f" old_state (অপরিবর্তিত রয়ে গেছে): {old_state}")
print(f" new_state: {new_state}")
print(f" id(old_state) != id(new_state): {id(old_state) != id(new_state)}")
print(f"\nচূড়ান্ত State: {store.state}")
SET_STEP) শুধু step বদলায়, count
অপরিবর্তিত (আগের মতোই 1) থাকে — তবুও id(old_state) != id(new_state) চেক
True হয়, কারণ {**state, "step": intent["value"]} সবসময় একটি সম্পূর্ণ নতুন
ডিকশনারি অবজেক্ট বানায়, শুধু একটি ফিল্ড বদলালেও। এটি L11-এর MVVM-এর Observable.set()-এর
আচরণ থেকে ভিন্ন — সেখানে মান সত্যিই অপরিবর্তিত থাকলে (সমতা-চেকের মাধ্যমে) কোনো নোটিফিকেশনই যায় না। MVI-তে
সমতা-চেক নেই — প্রতিটি Intent-ই একটি নতুন State প্রজন্ম তৈরি করে, এবং View প্রতিবারই (সস্তা হওয়ায়) পুরো
State রেন্ডার করে।
MVI-এর কেন্দ্রীয় নিয়ম — State কখনো in-place মিউটেট হয় না, প্রতিটি Intent একটি নতুন, স্বয়ংসম্পূর্ণ State অবজেক্ট তৈরি করে। এই কঠোর ইমিউটেবিলিটি ডিবাগিং সহজ করে (প্রতিটি পুরনো State এখনো অক্ষত থাকে, চাইলে তুলনা বা "টাইম-ট্র্যাভেল" করা যায়) এবং FSWF-এর Redux reducer প্যাটার্ন (L16)-এর সাথে ধারণাগতভাবে হুবহু মেলে — শুধু মোবাইল প্রেক্ষাপটে "Intent" নামকরণ ও প্রায়ই একক-স্ক্রিন-স্কোপড State ব্যবহারের প্রবণতা।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ MVI-এর "Intent" আর MVC-এর (L10) "টাচ ইভেন্ট"-এর মধ্যে পার্থক্য কী?
L10-এ টাচ ইভেন্ট সরাসরি একটি Controller মেথড কল করেছিল (যেমন on_edit_name(new_name)) — যা
চাইলে Model সরাসরি মিউটেট করতে পারতো। MVI-তে Intent শুধু একটি ডেটা অবজেক্ট (এখানে একটি
dict, {"type": ..., ...}) যা বর্ণনা করে "কী ঘটেছে" — এটি নিজে কোনো লজিক চালায় না, শুধু
reduce()-এর কাছে পাঠানো হয়। এই বিচ্ছিন্নতাই ইউনিডাইরেকশনাল ফ্লো-র মূল ভিত্তি।
প্র ০২
উপরের কোডে reduce() যদি ভুলবশত state["count"] += 1 লিখে সরাসরি ডিকশনারি মিউটেট করতো, তাহলে ইমিউটেবিলিটি চেকে কী দেখা যেত?
state["count"] += 1 একই ডিকশনারি অবজেক্টের ভেতরের মান বদলে দেয়, নতুন অবজেক্ট তৈরি করে না
— যদি এটি রিটার্ন করা হতো তাহলে new_state ও old_state একই অবজেক্টকে নির্দেশ
করতো (old_state-ও বদলে যেত, কারণ এটি একই অবজেক্টের রেফারেন্স), আর
id(old_state) != id(new_state) চেক False হতো — ইমিউটেবিলিটি নিয়ম ভাঙা
পড়ে যেত এবং "পুরনো State এখনো অক্ষত" এই গ্যারান্টিটাও হারিয়ে যেত।
প্র ০৩
উপরের কোডে অচেনা কোনো Intent (যেমন {"type": "UNKNOWN"}) পাঠালে id(old_state) != id(new_state) চেক কী ফলাফল দেবে?
False — কারণ reduce()-এর শেষ লাইন return state একই
অবজেক্ট (কোনো নতুন ডিকশনারি তৈরি না করেই) ফেরত দেয় যখন কোনো elif শাখা মেলে না। এটি সঠিক
আচরণ — যে Intent-এর জন্য কোনো নিয়ম নেই, তার জন্য State অপরিবর্তিত থাকা উচিত, এবং এখানে "অপরিবর্তিত"
মানে সত্যিই একই অবজেক্ট, একটি অপ্রয়োজনীয় নতুন কপি নয়।
অনুশীলন
-
চিন্তা করুন: একটি টু-ডু-টগল স্ক্রিনে (আইটেম লিস্টের একটি আইটেম done/not-done টগল করা) MVI
দিয়ে কীভাবে ডিজাইন করবেন — State-এ কী থাকবে, আর "টগল" Intent-টি কেমন দেখতে হবে বলে মনে হয়?
State হতে পারে
{"todos": [{"id": 1, "text": "...", "done": False}, ...]}। Intent হতে পারে{"type": "TOGGLE_TODO", "id": 1}।reduce()-এ এই Intent-এর জন্য একটি নতুনtodosলিস্ট তৈরি করতে হবে (যেমন একটি লিস্ট কম্প্রিহেনশন দিয়ে যা মিলে যাওয়া আইটেমেরdoneউল্টে দেয় আর বাকিগুলো অপরিবর্তিত রাখে) — মূল লিস্ট বা তার ভেতরের ডিকশনারিগুলো in-place মিউটেট না করে, ঠিক যেমন উপরেরcount/step-এর ক্ষেত্রে করা হয়েছিল। -
পরীক্ষা করুন: উপরের কোড সেলে
intentsলিস্টের শেষে{"type": "RESET"}যোগ করুন এবং আবার চালিয়ে দেখুন চূড়ান্ত State কী হয় ও পঞ্চম ইমিউটেবিলিটি চেকের ফলাফল কী আসে।পঞ্চম Intent
{"type": "RESET"}-এর জন্যreduce(){**state, "count": 0}রিটার্ন করবে — চতুর্থ ধাপ শেষে State ছিল{"count": 1, "step": 5}, তাই RESET-এর পর চূড়ান্ত State হবে{"count": 0, "step": 5}(লক্ষণীয়:step৫-ই থেকে যায়, শুধুcountশূন্য হয়)। যেহেতু এটিও{**state, ...}দিয়ে একটি নতুন ডিকশনারি তৈরি করে, পঞ্চমid(old_state) != id(new_state)চেকও True হবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাপ লাইফসাইকেল, মোবাইল UI/UX, MVVM/MVI আর্কিটেকচার, নেটিভ বনাম ক্রস-প্ল্যাটফর্ম, নেভিগেশন, স্টেট ম্যানেজমেন্ট, লোকাল স্টোরেজ, ডিভাইস ফিচার ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- FSWF · The Flux/Redux Pattern সহোদর পাঠ ইউনিডাইরেকশনাল ডেটা-ফ্লোর সাধারণ সংজ্ঞা ও Action/Dispatcher/Store কাঠামো এই পাঠেই তৈরি হয়েছে।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks ও Mobile App Development — সব এক জায়গায়।