পাঠ ২৯ · ৫৭-এর মধ্যে · মডিউল ৭
Home / Courses / Mobile App Development / স্টেট ম্যানেজমেন্ট

লাইফসাইকেল-অ্যাওয়্যার স্টেট ম্যানেজমেন্ট

Lifecycle-aware state management
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • কেন একটি ব্যাকগ্রাউন্ড স্ক্রিনের স্টোর-সাবস্ক্রিপশন সক্রিয় রাখা ক্ষতিকর
  • L01-এর AppLifecycle ও L28-এর Store — এই দুটি প্যাটার্ন একসাথে যুক্ত করা
  • একটি স্ক্রিনের লাইফসাইকেল ট্রানজিশনের ভেতরেই সাবস্ক্রাইব/আনসাবস্ক্রাইব করা কোড লেখা
  • সাবস্ক্রাইবার তালিকা ও প্রাপ্ত-আপডেট লগ পরীক্ষা করে সত্যিকারের প্রমাণ যাচাই করা

১ · সমস্যা: সাবস্ক্রিপশন লাইফসাইকেলের সাথে যুক্ত না থাকলে

L28-এ যে Store ক্লাস দেখা হয়েছিল, সেখানে একটি স্ক্রিন একবার subscribe() করলে তার callback চিরকাল স্টোরের প্রতিটি আপডেটে কল হতে থাকে — স্ক্রিনটি এখনো স্ক্রিনে দেখা যাচ্ছে কি না তা বিবেচনা না করেই। বাস্তবে এর মানে: ব্যবহারকারী ইনবক্স স্ক্রিন ছেড়ে অন্য স্ক্রিনে চলে গেলেও, ইনবক্সের ডেটা-রিফ্রেশ callback পেছনে পেছনে চলতেই থাকবে — অদরকারি কাজ, অদরকারি ব্যাটারি খরচ, এবং (L47-এ বিস্তারিত) একটি সম্ভাব্য মেমোরি লিক।

সমাধান — একটি স্ক্রিনের সাবস্ক্রিপশনকে তার নিজস্ব AppLifecycle-স্টাইল স্টেট মেশিনের সাথে বেঁধে দেওয়া: foreground-এ ঢোকার মুহূর্তে subscribe() কল হবে, আর foreground থেকে background-এ যাওয়ার মুহূর্তে unsubscribe() কল হবে।

২ · একটি লাইফসাইকেল-অ্যাওয়্যার স্ক্রিন

নিচের ক্লাসটি L01-এর AppLifecycle-স্টাইল ALLOWED ট্রানজিশন টেবিল এবং L28-এর Store — দুটোকেই একসাথে ব্যবহার করে। প্রতিটি ট্রানজিশনের ভেতরেই সাবস্ক্রাইব/আনসাবস্ক্রাইব করা হয়, আলাদা কোনো "মনে করে পরে ডিসকানেক্ট করা" ধাপ হিসেবে নয়।

Python
# L28-এর Store, অপরিবর্তিত
class Store:
    def __init__(self, initial_state):
        self.state = dict(initial_state)
        self._subscribers = []

    def set_state(self, partial):
        self.state = {**self.state, **partial}
        for listener in list(self._subscribers):
            listener(self.state)

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

    def unsubscribe(self, listener):
        if listener in self._subscribers:
            self._subscribers.remove(listener)


# L01-এর AppLifecycle-স্টাইল ট্রানজিশন টেবিল + Store-সাবস্ক্রিপশন একসাথে যুক্ত
class LifecycleAwareScreen:
    ALLOWED = {
        'not_running': {'foreground'},
        'foreground':  {'background'},
        'background':  {'foreground', 'killed'},
        'killed':      set(),
    }

    def __init__(self, name, store):
        self.name = name
        self.store = store
        self.state = 'not_running'
        self.received_updates = []  # এই স্ক্রিনের callback সত্যিই যা যা পেয়েছে তার লগ

    def _on_store_change(self, new_state):
        self.received_updates.append(dict(new_state))
        print(f"      -> [{self.name}] callback invoke হলো, state: {new_state}")

    def transition(self, new_state, reason):
        if new_state not in self.ALLOWED[self.state]:
            print(f"{self.name:20s} | {reason:36s} | {self.state} -> {new_state}  (অবৈধ)")
            return False

        old_state = self.state
        self.state = new_state

        if new_state == 'foreground':
            self.store.subscribe(self._on_store_change)
            note = "[subscribe করা হলো]"
        elif old_state == 'foreground' and new_state == 'background':
            self.store.unsubscribe(self._on_store_change)
            note = "[unsubscribe করা হলো]"
        else:
            note = ""

        print(f"{self.name:20s} | {reason:36s} | {old_state} -> {new_state}  (বৈধ) {note}")
        return True

    

৩ · প্রমাণ: ব্যাকগ্রাউন্ড স্ক্রিনের callback সত্যিই কল হয় না

এবার দুটি স্বতন্ত্র স্ক্রিন — ইনবক্স ও প্রোফাইল — একই স্টোরের সাথে যুক্ত করা হবে। ইনবক্স প্রথমে ফোরগ্রাউন্ডে আসে, একটি আপডেট পায়, তারপর ব্যাকগ্রাউন্ডে চলে যায় (আনসাবস্ক্রাইব হয়ে)। এরপর প্রোফাইল ফোরগ্রাউন্ডে আসে এবং স্টোরে আরেকটি আপডেট আসে — কোন স্ক্রিন সেটি সত্যিই পায়, তা কোডের প্রকৃত রান-টাইম আচরণ থেকে যাচাই করা হবে, শুধু ধরে নেওয়া নয়।

Python
store = Store({"unread_count": 0})

inbox = LifecycleAwareScreen("ইনবক্স স্ক্রিন", store)
profile = LifecycleAwareScreen("প্রোফাইল স্ক্রিন", store)

def subscriber_names():
    return [cb.__self__.name for cb in store._subscribers]

print("--- ধাপ ১: ইনবক্স স্ক্রিন খোলা হলো ---")
inbox.transition('foreground', 'ইউজার ইনবক্সে প্রবেশ করলো')
print(f"   সাবস্ক্রাইবার তালিকা: {subscriber_names()}")

print("\n--- ধাপ ২: একটি নতুন মেসেজ এলো, store-এ আপডেট হলো ---")
store.set_state({"unread_count": 1})

print("\n--- ধাপ ৩: ইউজার হোম বাটন চাপলো, ইনবক্স ব্যাকগ্রাউন্ডে গেলো ---")
inbox.transition('background', 'ইউজার হোম বাটন চাপলো')
print(f"   সাবস্ক্রাইবার তালিকা: {subscriber_names()}")

print("\n--- ধাপ ৪: ইউজার প্রোফাইল স্ক্রিন খুললো ---")
profile.transition('foreground', 'ইউজার প্রোফাইল স্ক্রিনে গেলো')
print(f"   সাবস্ক্রাইবার তালিকা: {subscriber_names()}")

print("\n--- ধাপ ৫: আরেকটি নতুন মেসেজ এলো, store-এ আবার আপডেট হলো ---")
store.set_state({"unread_count": 2})

print(f"\nইনবক্স স্ক্রিন যা যা আপডেট পেয়েছে: {inbox.received_updates}")
print(f"প্রোফাইল স্ক্রিন যা যা আপডেট পেয়েছে: {profile.received_updates}")

inbox_missed_last_update = len(inbox.received_updates) == 1
profile_received_last_update = profile.received_updates == [{"unread_count": 2}]

print(f"\nপ্রমাণ ১ -- ব্যাকগ্রাউন্ডে থাকা ইনবক্স স্ক্রিন ধাপ-৫-এর আপডেট পায়নি (মোট মাত্র ১টি আপডেট পেয়েছে, ধাপ-২-এর): {inbox_missed_last_update}")
print(f"প্রমাণ ২ -- ফোরগ্রাউন্ডে থাকা প্রোফাইল স্ক্রিন ধাপ-৫-এর আপডেট সঠিকভাবে পেয়েছে: {profile_received_last_update}")
assert inbox_missed_last_update and profile_received_last_update
print("দুটি প্রমাণই সত্য -- assert ব্যর্থ হয়নি।")

    
ধাপ ৩-এ ইনবক্স ব্যাকগ্রাউন্ডে যাওয়ার সাথে সাথেই unsubscribe() কল হয়ে স্টোরের _subscribers লিস্ট থেকে তার callback সরে যায় — তাই ধাপ ৫-এর set_state শুধু _subscribers লিস্টে যা যা তখন সত্যিই আছে তাদেরই কল করে, যা সেই মুহূর্তে শুধু প্রোফাইল স্ক্রিন। ইনবক্সের received_updates লিস্ট ধাপ ৩-এর পরে আর কখনো বাড়েনি — এটি অনুমান নয়, প্রকৃত রান-টাইম ফলাফল।
মূল কথা · Key takeaway

একটি সাবস্ক্রিপশনের জীবনকাল সবসময় সেই স্ক্রিনের দৃশ্যমানতার জীবনকালের সাথে বাঁধা থাকা উচিত। লাইফসাইকেল ট্রানজিশনের ভেতরেই subscribe/unsubscribe কল করলে ব্যাকগ্রাউন্ড স্ক্রিন কখনো অদরকারি কাজ করে না, আর কোনো ম্যানুয়াল "মনে করে ক্লিনআপ করা" ধাপের উপর নির্ভর করতে হয় না। L47-এ দেখা যাবে এই একই আনসাবস্ক্রাইব-না-করার ভুল কীভাবে একটি প্রকৃত মেমোরি লিক তৈরি করে।

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

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

প্র ০১ ইনবক্স স্ক্রিন যদি আবার foreground-এ ফিরে আসে, তাহলে কি সে আবার স্টোরের আপডেট পাবে?

হ্যাঁ — transition('foreground', ...) আবার কল হলে subscribe()-ও আবার কল হবে, এবং তার callback আবার _subscribers লিস্টে যুক্ত হয়ে যাবে। তখন থেকে ভবিষ্যতের যেকোনো set_state কলে সে আবার আপডেট পাবে — কিন্তু ব্যাকগ্রাউন্ডে থাকা সময়টায় যা যা আপডেট হয়ে গেছে, সেগুলো সে মিস করেছে (এই মিসড-আপডেট সমস্যাটিই L31 সমাধান করবে, স্টেট রিস্টোরেশনের মাধ্যমে)।

প্র ০২ যদি unsubscribe() কখনো কল না হতো (ভুলবশত), তাহলে সাবস্ক্রাইবার লিস্টে কী হতো?

ইনবক্স স্ক্রিন প্রতিবার ফোরগ্রাউন্ডে আসা-যাওয়া করলে তার callback প্রতিবার নতুন করে লিস্টে যুক্ত হতো, কখনো সরানো হতো না — একই ইনবক্স স্ক্রিনের callback একাধিকবার লিস্টে থাকলে প্রতিটি আপডেটে সেই callback একাধিকবার কল হতো (ডুপ্লিকেট কাজ), এবং লিস্টের আকার প্রতিটি এন্ট্রি-এক্সিটে অনির্দিষ্টকালের জন্য বাড়তেই থাকত — ঠিক এই সমস্যাটিই L47-এ মেমোরি লিক হিসেবে বিস্তারিত দেখানো হবে।

প্র ০৩ দ্বিতীয় কোড সেলের ধাপ ৪-এ প্রোফাইল ফোরগ্রাউন্ডে আসার সময় সাবস্ক্রাইবার তালিকায় কে কে ছিল, এবং কেন ইনবক্স সেখানে ছিল না?

ধাপ ৪-এর পরে সাবস্ক্রাইবার তালিকায় শুধু ['প্রোফাইল স্ক্রিন'] ছিল। ইনবক্স ছিল না, কারণ ধাপ ৩-এই তার transition('background', ...) কলটি তার callback-কে _subscribers লিস্ট থেকে ইতিমধ্যে সরিয়ে দিয়েছিল — সেই মুহূর্ত থেকে ইনবক্স আর স্টোরের কোনো নোটিফিকেশন পায় না, যতক্ষণ না সে আবার ফোরগ্রাউন্ডে ফেরে।

অনুশীলন

  1. চিন্তা করুন: একটি সঙ্গীত-প্লেয়ার অ্যাপে "এখন যা বাজছে" দেখানো স্ক্রিনটি ব্যাকগ্রাউন্ডে গেলেও কি তার স্টোর-সাবস্ক্রিপশন সক্রিয় রাখা উচিত? কেন সাধারণ ইনবক্স-রিফ্রেশ callback-এর নিয়মটি এখানে একইভাবে প্রযোজ্য নাও হতে পারে?

    সাধারণত না — কিন্তু ব্যতিক্রম আছে। "এখন যা বাজছে"-এর মতো একটি নোটিফিকেশন/লক-স্ক্রিন উইজেট, যা ব্যাকগ্রাউন্ডেও দৃশ্যমান থাকে, তার নিজস্ব ভিন্ন লাইফসাইকেল থাকতে পারে (স্ক্রিন নিজে অদৃশ্য, কিন্তু একটি সিস্টেম উইজেট এখনো সক্রিয়)। মূলনীতি একই থাকে — সাবস্ক্রিপশনটি যেটির প্রকৃত দৃশ্যমান জীবনকালের সাথে বাঁধা, সেটির সাথেই বাঁধা উচিত, ব্লাইন্ডলি স্ক্রিনের লাইফসাইকেলের সাথে নয়।

  2. পরীক্ষা করুন: দ্বিতীয় কোড সেলে ধাপ ৫-এর ঠিক পরে ইনবক্সকে আবার ফোরগ্রাউন্ডে ফেরান — inbox.transition('foreground', 'ইউজার আবার ইনবক্সে ফিরে এলো') — তারপর একটি তৃতীয় আপডেট পাঠান — store.set_state({"unread_count": 3}) — এবং দেখুন inbox.received_updates-এ এবার কী যুক্ত হয়।

    ইনবক্স আবার ফোরগ্রাউন্ডে আসার সাথে সাথে পুনরায় সাবস্ক্রাইব হয়ে যাবে, তাই তৃতীয় set_state-এর {"unread_count": 3} মানটি তার received_updates লিস্টে যুক্ত হবে — লিস্টটি তখন [{'unread_count': 1}, {'unread_count': 3}] হবে। লক্ষ্য করুন মাঝের {'unread_count': 2} আপডেটটি (ধাপ ৫) সে কখনোই পায়নি — কারণ তখন সে আনসাবস্ক্রাইবড ছিল।

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

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