পাঠ ১২ · ৫৭-এর মধ্যে · মডিউল ৩

MVI ও ইউনিডাইরেকশনাল ডেটা ফ্লো

MVI and unidirectional data flow
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • 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 ক্লাসের সাথে নাম মিললেও এটি ভিন্ন একটি ধারণা)। চক্রটি হুবহু একই: এক দিকেই প্রবাহিত হয়, কখনো উল্টো দিকে নয়।

Intent {"type": "INCREMENT"} reduce(state, intent) সবসময় নতুন State অবজেক্ট রিটার্ন করে View নতুন State অনুযায়ী রি-রেন্ডার করে
Intent সবসময় reduce ফাংশনে যায়, reduce একটি সম্পূর্ণ নতুন State অবজেক্ট হিসেব করে, View নতুন State রেন্ডার করে, এবং View-তে ইউজারের ইন্টারঅ্যাকশন নতুন Intent তৈরি করে চক্রটি আবার শুরু করে।

২ · একটি কাউন্টার স্ক্রিনের MVI — বাস্তব কোড

নিচের কোডে reduce() কখনো state-এর ভেতরের ডিকশনারি in-place পরিবর্তন করে না — প্রতিবার {**state, ...} দিয়ে একটি নতুন ডিকশনারি তৈরি করে রিটার্ন করে। চারটি Intent ক্রমান্বয়ে dispatch করে প্রতিবার id(old_state) != id(new_state) চেক করে যাচাই করা হচ্ছে যে সত্যিই একটি নতুন অবজেক্ট তৈরি হয়েছে, পুরনোটা মিউটেট হয়নি।

Python
# ---------- 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}")

    
লক্ষ্য করুন চতুর্থ Intent-এ (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 রেন্ডার করে।
মূল কথা · Key takeaway

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 অপরিবর্তিত থাকা উচিত, এবং এখানে "অপরিবর্তিত" মানে সত্যিই একই অবজেক্ট, একটি অপ্রয়োজনীয় নতুন কপি নয়।

অনুশীলন

  1. চিন্তা করুন: একটি টু-ডু-টগল স্ক্রিনে (আইটেম লিস্টের একটি আইটেম 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-এর ক্ষেত্রে করা হয়েছিল।

  2. পরীক্ষা করুন: উপরের কোড সেলে 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 — সব এক জায়গায়।
আগের পাঠ
MVVM ও ডেটা বাইন্ডিং