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

MVVM ও ডেটা বাইন্ডিং

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

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

  • MVC-এর "পুরো স্ক্রিন রি-রেন্ডার" থেকে MVVM-এর "প্রতি-প্রপার্টি বাইন্ডিং" কীভাবে ভিন্ন
  • একটি লগইন স্ক্রিনের জন্য username, password, ও ডিরাইভড is_login_enabled — তিনটি Observable প্রপার্টি ডিজাইন করা
  • একটি সত্যিকারের callback-based বাইন্ডিং যেখানে প্রপার্টি বদলালেই View-এর নির্দিষ্ট অংশ নিজে থেকে আপডেট হয়
  • কেন মোবাইল প্ল্যাটফর্মে MVVM এত জনপ্রিয় — লাইফসাইকেল-অ্যাওয়্যার বাইন্ডিং টুলের সংক্ষিপ্ত পরিচিতি

১ · প্রতি-প্রপার্টি বাইন্ডিং বনাম পুরো-স্ক্রিন রি-রেন্ডার

FSWF L06-এ দেখানো হয়েছে Observable ক্লাস কীভাবে একটি মান ধারণ করে ও মান বদলালে সাবস্ক্রাইবারদের জানায়। L10-এর ProfileController প্রতিটি এডিটের পর পুরো View-কে নিঃশর্তে রি-রেন্ডার করতে বলেছিল। মোবাইল লগইন স্ক্রিনে এই পদ্ধতি অপচয়ী — ইউজারনেম ফিল্ড বদলালে পাসওয়ার্ড ফিল্ড বা লগইন বাটনকে কেন আবার আঁকতে হবে? MVVM-এ প্রতিটি Observable প্রপার্টি শুধু তার নিজের UI অংশের সাথে বাইন্ড থাকে।

LoginViewModel username, password, is_login_enabled username ফিল্ড শুধু username বদলালে আপডেট password ফিল্ড শুধু password বদলালে আপডেট লগইন বাটন শুধু is_login_enabled বদলালে আপডেট
প্রতিটি Observable প্রপার্টির নিজস্ব সাবস্ক্রাইবার আছে -- একটি প্রপার্টি বদলালে শুধু তার সাথে বাইন্ড করা UI অংশ আপডেট হয়, পুরো স্ক্রিন নয়।

২ · একটি লগইন স্ক্রিনের MVVM — বাস্তব কোড

নিচের কোডে Observable ক্লাসটি L10-এর মতো নয় — মান আসলেই বদলালেই সাবস্ক্রাইবারদের জানায় (old_value != new_value চেক)। LoginViewModel-এর is_login_enabled একটি ডিরাইভড প্রপার্টি — এটি সরাসরি ইউজার-ইনপুট নয়, বরং username ও password-এর বর্তমান মান থেকে প্রতিবার পুনরায় হিসেব করা হয়।

Python
# ---------- একটি বাইন্ডেবল (observable) প্রপার্টি -- মান সত্যিই বদলালেই জানায় ----------
class Observable:
    def __init__(self, initial_value):
        self._value = initial_value
        self._listeners = []

    def get(self):
        return self._value

    def set(self, new_value):
        old_value = self._value
        self._value = new_value
        if old_value != new_value:
            for listener in self._listeners:
                listener(new_value)

    def subscribe(self, listener):
        self._listeners.append(listener)


# ---------- VIEWMODEL -- তিনটি বাইন্ডেবল প্রপার্টি এক্সপোজ করে, View সম্পর্কে কিছুই জানে না ----------
class LoginViewModel:
    def __init__(self):
        self.username = Observable("")
        self.password = Observable("")
        self.is_login_enabled = Observable(False)

    def set_username(self, value):
        self.username.set(value)
        self._recompute_login_enabled()

    def set_password(self, value):
        self.password.set(value)
        self._recompute_login_enabled()

    def _recompute_login_enabled(self):
        enabled = len(self.username.get()) > 0 and len(self.password.get()) >= 6
        self.is_login_enabled.set(enabled)   # শুধু আসলে বদলালেই বাটন-বাইন্ডিং নোটিফাই হবে


# ---------- VIEW -- প্রতিটি প্রপার্টির সাথে আলাদাভাবে বাইন্ড করে ----------
class LoginView:
    def __init__(self, view_model):
        self.vm = view_model
        self.updates = {"username_field": 0, "password_field": 0, "login_button": 0}

        self.vm.username.subscribe(self._on_username_changed)
        self.vm.password.subscribe(self._on_password_changed)
        self.vm.is_login_enabled.subscribe(self._on_login_enabled_changed)

        # প্রাথমিক বাইন্ডিং -- স্ক্রিন খোলার সময় প্রতিটি অংশের প্রথম রেন্ডার
        self._on_username_changed(self.vm.username.get())
        self._on_password_changed(self.vm.password.get())
        self._on_login_enabled_changed(self.vm.is_login_enabled.get())

    def _on_username_changed(self, value):
        self.updates["username_field"] += 1
        print(f"[username ফিল্ড আপডেট #{self.updates['username_field']}] '{value}'")

    def _on_password_changed(self, value):
        self.updates["password_field"] += 1
        masked = "•" * len(value)
        print(f"[password ফিল্ড আপডেট #{self.updates['password_field']}] '{masked}' ({len(value)} অক্ষর)")

    def _on_login_enabled_changed(self, enabled):
        self.updates["login_button"] += 1
        state = "সক্রিয় (enabled)" if enabled else "নিষ্ক্রিয় (disabled)"
        print(f"[লগইন বাটন আপডেট #{self.updates['login_button']}] {state}")


print("== লগইন স্ক্রিন খোলা হলো ==")
vm = LoginViewModel()
view = LoginView(vm)

print("\n== ইউজার ইউজারনেম টাইপ করলো: 'karim' ==")
vm.set_username("karim")            # username_field আপডেট হয়; is_login_enabled এখনো False -- বাটন বদলায় না

print("\n== ইউজার পাসওয়ার্ড টাইপ করলো: '12345' (৫ অক্ষর, যথেষ্ট নয়) ==")
vm.set_password("12345")            # password_field আপডেট হয়; is_login_enabled এখনো False -- বাটন বদলায় না

print("\n== ইউজার পাসওয়ার্ড ঠিক করলো: '123456' (৬ অক্ষর) ==")
vm.set_password("123456")           # password_field আপডেট হয়; is_login_enabled এখন True -- বাটন আপডেট হয়

print(f"\nমোট আপডেট: {view.updates}")

    
লক্ষ্য করুন প্রথম দুটি পরিবর্তনের সময় (set_username("karim") ও set_password("12345")) লগইন বাটন একবারও আপডেট হয় না — কারণ is_login_enabled-এর মান দুবারই False-ই থাকে (Observable.set()-এর old_value != new_value চেক ব্যর্থ হয়, তাই কোনো নোটিফিকেশন যায় না)। শুধু তৃতীয় পরিবর্তনে (পাসওয়ার্ড ৬ অক্ষরে পৌঁছালে) is_login_enabled সত্যিই False → True বদলায়, তাই বাটন ঠিক তখনই আপডেট হয়। শেষ পর্যন্ত updates: username_field=2 (১টি প্রাথমিক + ১টি পরিবর্তন), password_field=3 (১টি প্রাথমিক + ২টি পরিবর্তন), login_button=2 (১টি প্রাথমিক + ১টি পরিবর্তন)।
মূল কথা · Key takeaway

MVVM-এ ViewModel বাইন্ডেবল স্টেট এক্সপোজ করে আর View স্বেচ্ছায় সেই স্টেট "অবজার্ভ" করে — ViewModel কখনো জানে না কে বা কয়টি UI অংশ তার প্রপার্টি বাইন্ড করে রেখেছে। এই ডিকাপলিং আর প্রতি-প্রপার্টি নির্ভুল আপডেট ছোট মোবাইল স্ক্রিনে (সীমিত ব্যাটারি/CPU) MVC-এর নিঃশর্ত রি-রেন্ডারের চেয়ে বেশি কার্যকর — এবং এটিই কেন Android/iOS-এর নেটিভ ডেটা-বাইন্ডিং টুল এই একই ধারণার ওপর ভিত্তি করে তৈরি।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ is_login_enabled-কে কেন একটি "ডিরাইভড" প্রপার্টি বলা হচ্ছে?

কারণ ইউজার সরাসরি এর মান টাইপ করে না — এটি প্রতিবার username বা password বদলালে _recompute_login_enabled()-এর মাধ্যমে সেই দুটি প্রপার্টির বর্তমান মান থেকে নতুন করে হিসেব করা হয়। ডিরাইভড প্রপার্টি ব্যবহার করলে "লগইন বাটন কখন সক্রিয় হবে" এই নিয়মটি একটিমাত্র জায়গায় (_recompute_login_enabled) থাকে — View-এর কোনো অংশে এই লজিক ছড়িয়ে থাকে না।

প্র ০২ L10-এর MVC আর এই পাঠের MVVM — দুটোই তো Controller/ViewModel-এ লজিক রাখে। আসল পার্থক্যটা তাহলে কোথায়?

পার্থক্যটা View কীভাবে জানে কখন রি-রেন্ডার করতে হবে তাতে। MVC-তে Controller-কে সক্রিয়ভাবে view.render() কল করতে হয় — ভুলে গেলে UI stale থেকে যায় (L10-এর অনুশীলনে দেখা গেছে)। MVVM-এ View নিজেই ViewModel-এর প্রপার্টি অবজার্ভ করে — ViewModel কখনো View-কে সরাসরি কল করে না, বরং শুধু মান বদলালেই সাবস্ক্রাইবাররা স্বয়ংক্রিয়ভাবে জানতে পারে। এটি "push" (Controller ঠেলে দেয়) বনাম "observe" (View নিজে দেখে) — এই দুই ভিন্ন মডেল।

প্র ০৩ যদি Observable.set()-এ old_value != new_value চেকটি না থাকতো, তাহলে উপরের কোডে মোট কতগুলো বাটন-আপডেট হতো?

তিনটি পরিবর্তনের প্রতিটিতেই _recompute_login_enabled() চলে এবং প্রতিবারই is_login_enabled.set(...) কল হয় — চেক না থাকলে প্রতিটি কলই নোটিফাই করতো, ফলে প্রাথমিক রেন্ডারসহ মোট ৪টি বাটন-আপডেট হতো (এখন যেখানে মাত্র ২টি হয়) — যদিও বাস্তবে বাটনের অবস্থা মাত্র একবারই সত্যিকারভাবে বদলেছে (disabled → enabled)। এটিই দেখায় কেন সমতা-চেকটি অপ্রয়োজনীয় UI কাজ এড়াতে জরুরি।

অনুশীলন

  1. চিন্তা করুন: বাস্তব একটি লগইন স্ক্রিনে "পাসওয়ার্ড দেখাও/লুকাও" (show/hide password) টগল বাটন থাকে। এটিকে কীভাবে LoginViewModel-এ একটি নতুন Observable প্রপার্টি হিসেবে যোগ করবেন বলে মনে হয়?

    একটি নতুন self.is_password_visible = Observable(False) প্রপার্টি ও একটি toggle_password_visibility() মেথড যোগ করা যেতে পারে যা self.is_password_visible.set(not self.is_password_visible.get()) কল করবে। View-এ এই প্রপার্টির একটি নতুন সাবস্ক্রাইবার থাকবে যা পাসওয়ার্ড ফিল্ডের masking (• বনাম আসল অক্ষর) টগল করবে — লক্ষণীয় যে এটি সম্পূর্ণ স্বাধীন একটি প্রপার্টি, username/ password-এর সাথে সরাসরি সম্পর্কহীন, তাই এটি বদলালে শুধু masking-বাইন্ডিং আপডেট হবে, username/password ফিল্ড বা লগইন বাটন নয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে সবশেষে vm.set_username("") — অর্থাৎ ইউজারনেম খালি করে দিন — এই লাইনটি যোগ করে আবার চালান, এবং দেখুন updates["login_button"]-এর চূড়ান্ত মান কত হয়।

    vm.set_username("") কল হলে username "karim" থেকে "" -এ বদলায় ( username_field আপডেট হয়), এবং _recompute_login_enabled() আবার চলে: len("") > 0 এখন False, তাই enabled হয়ে যায় False — যেহেতু আগের মান True ছিল, এটি একটি প্রকৃত পরিবর্তন, তাই login_button আবার আপডেট হয়ে updates["login_button"] ২ থেকে ৩-এ পৌঁছাবে (disabled দেখিয়ে)।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাপ লাইফসাইকেল, মোবাইল UI/UX, MVVM/MVI আর্কিটেকচার, নেটিভ বনাম ক্রস-প্ল্যাটফর্ম, নেভিগেশন, স্টেট ম্যানেজমেন্ট, লোকাল স্টোরেজ, ডিভাইস ফিচার ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • FSWF · MVVM and MVP Patterns সহোদর পাঠ Observable প্যাটার্নের সাধারণ সংজ্ঞা ও MVP-এর সাথে তুলনা এই পাঠেই তৈরি হয়েছে।
  • সব 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 — সব এক জায়গায়।
আগের পাঠ
মোবাইল অ্যাপের জন্য MVC