লাইফসাইকেল-অ্যাওয়্যার স্টেট ম্যানেজমেন্ট
এই পাঠে যা শিখবেন
- কেন একটি ব্যাকগ্রাউন্ড স্ক্রিনের স্টোর-সাবস্ক্রিপশন সক্রিয় রাখা ক্ষতিকর
- L01-এর
AppLifecycleও L28-এরStore— এই দুটি প্যাটার্ন একসাথে যুক্ত করা - একটি স্ক্রিনের লাইফসাইকেল ট্রানজিশনের ভেতরেই সাবস্ক্রাইব/আনসাবস্ক্রাইব করা কোড লেখা
- সাবস্ক্রাইবার তালিকা ও প্রাপ্ত-আপডেট লগ পরীক্ষা করে সত্যিকারের প্রমাণ যাচাই করা
১ · সমস্যা: সাবস্ক্রিপশন লাইফসাইকেলের সাথে যুক্ত না থাকলে
L28-এ যে Store ক্লাস দেখা হয়েছিল, সেখানে একটি স্ক্রিন একবার subscribe() করলে তার
callback চিরকাল স্টোরের প্রতিটি আপডেটে কল হতে থাকে — স্ক্রিনটি এখনো স্ক্রিনে দেখা যাচ্ছে কি না তা বিবেচনা না
করেই। বাস্তবে এর মানে: ব্যবহারকারী ইনবক্স স্ক্রিন ছেড়ে অন্য স্ক্রিনে চলে গেলেও, ইনবক্সের ডেটা-রিফ্রেশ callback
পেছনে পেছনে চলতেই থাকবে — অদরকারি কাজ, অদরকারি ব্যাটারি খরচ, এবং (L47-এ বিস্তারিত) একটি সম্ভাব্য মেমোরি লিক।
সমাধান — একটি স্ক্রিনের সাবস্ক্রিপশনকে তার নিজস্ব AppLifecycle-স্টাইল স্টেট মেশিনের সাথে বেঁধে
দেওয়া: foreground-এ ঢোকার মুহূর্তে subscribe() কল হবে, আর foreground
থেকে background-এ যাওয়ার মুহূর্তে unsubscribe() কল হবে।
২ · একটি লাইফসাইকেল-অ্যাওয়্যার স্ক্রিন
নিচের ক্লাসটি L01-এর AppLifecycle-স্টাইল ALLOWED ট্রানজিশন টেবিল এবং L28-এর
Store — দুটোকেই একসাথে ব্যবহার করে। প্রতিটি ট্রানজিশনের ভেতরেই সাবস্ক্রাইব/আনসাবস্ক্রাইব করা হয়,
আলাদা কোনো "মনে করে পরে ডিসকানেক্ট করা" ধাপ হিসেবে নয়।
# 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 সত্যিই কল হয় না
এবার দুটি স্বতন্ত্র স্ক্রিন — ইনবক্স ও প্রোফাইল — একই স্টোরের সাথে যুক্ত করা হবে। ইনবক্স প্রথমে ফোরগ্রাউন্ডে আসে, একটি আপডেট পায়, তারপর ব্যাকগ্রাউন্ডে চলে যায় (আনসাবস্ক্রাইব হয়ে)। এরপর প্রোফাইল ফোরগ্রাউন্ডে আসে এবং স্টোরে আরেকটি আপডেট আসে — কোন স্ক্রিন সেটি সত্যিই পায়, তা কোডের প্রকৃত রান-টাইম আচরণ থেকে যাচাই করা হবে, শুধু ধরে নেওয়া নয়।
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 লিস্ট ধাপ ৩-এর পরে আর কখনো বাড়েনি — এটি
অনুমান নয়, প্রকৃত রান-টাইম ফলাফল।
একটি সাবস্ক্রিপশনের জীবনকাল সবসময় সেই স্ক্রিনের দৃশ্যমানতার জীবনকালের সাথে বাঁধা থাকা উচিত। লাইফসাইকেল
ট্রানজিশনের ভেতরেই subscribe/unsubscribe কল করলে ব্যাকগ্রাউন্ড স্ক্রিন কখনো
অদরকারি কাজ করে না, আর কোনো ম্যানুয়াল "মনে করে ক্লিনআপ করা" ধাপের উপর নির্ভর করতে হয় না। L47-এ দেখা যাবে
এই একই আনসাবস্ক্রাইব-না-করার ভুল কীভাবে একটি প্রকৃত মেমোরি লিক তৈরি করে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
ইনবক্স স্ক্রিন যদি আবার foreground-এ ফিরে আসে, তাহলে কি সে আবার স্টোরের আপডেট পাবে?
হ্যাঁ — transition('foreground', ...) আবার কল হলে subscribe()-ও আবার কল হবে,
এবং তার callback আবার _subscribers লিস্টে যুক্ত হয়ে যাবে। তখন থেকে ভবিষ্যতের যেকোনো
set_state কলে সে আবার আপডেট পাবে — কিন্তু ব্যাকগ্রাউন্ডে থাকা সময়টায় যা যা আপডেট হয়ে গেছে,
সেগুলো সে মিস করেছে (এই মিসড-আপডেট সমস্যাটিই L31 সমাধান করবে, স্টেট রিস্টোরেশনের মাধ্যমে)।
প্র ০২
যদি unsubscribe() কখনো কল না হতো (ভুলবশত), তাহলে সাবস্ক্রাইবার লিস্টে কী হতো?
ইনবক্স স্ক্রিন প্রতিবার ফোরগ্রাউন্ডে আসা-যাওয়া করলে তার callback প্রতিবার নতুন করে লিস্টে যুক্ত হতো, কখনো সরানো হতো না — একই ইনবক্স স্ক্রিনের callback একাধিকবার লিস্টে থাকলে প্রতিটি আপডেটে সেই callback একাধিকবার কল হতো (ডুপ্লিকেট কাজ), এবং লিস্টের আকার প্রতিটি এন্ট্রি-এক্সিটে অনির্দিষ্টকালের জন্য বাড়তেই থাকত — ঠিক এই সমস্যাটিই L47-এ মেমোরি লিক হিসেবে বিস্তারিত দেখানো হবে।
প্র ০৩ দ্বিতীয় কোড সেলের ধাপ ৪-এ প্রোফাইল ফোরগ্রাউন্ডে আসার সময় সাবস্ক্রাইবার তালিকায় কে কে ছিল, এবং কেন ইনবক্স সেখানে ছিল না?
ধাপ ৪-এর পরে সাবস্ক্রাইবার তালিকায় শুধু ['প্রোফাইল স্ক্রিন'] ছিল। ইনবক্স ছিল না, কারণ ধাপ
৩-এই তার transition('background', ...) কলটি তার callback-কে _subscribers
লিস্ট থেকে ইতিমধ্যে সরিয়ে দিয়েছিল — সেই মুহূর্ত থেকে ইনবক্স আর স্টোরের কোনো নোটিফিকেশন পায় না, যতক্ষণ না
সে আবার ফোরগ্রাউন্ডে ফেরে।
অনুশীলন
-
চিন্তা করুন: একটি সঙ্গীত-প্লেয়ার অ্যাপে "এখন যা বাজছে" দেখানো স্ক্রিনটি ব্যাকগ্রাউন্ডে
গেলেও কি তার স্টোর-সাবস্ক্রিপশন সক্রিয় রাখা উচিত? কেন সাধারণ ইনবক্স-রিফ্রেশ callback-এর নিয়মটি এখানে
একইভাবে প্রযোজ্য নাও হতে পারে?
সাধারণত না — কিন্তু ব্যতিক্রম আছে। "এখন যা বাজছে"-এর মতো একটি নোটিফিকেশন/লক-স্ক্রিন উইজেট, যা ব্যাকগ্রাউন্ডেও দৃশ্যমান থাকে, তার নিজস্ব ভিন্ন লাইফসাইকেল থাকতে পারে (স্ক্রিন নিজে অদৃশ্য, কিন্তু একটি সিস্টেম উইজেট এখনো সক্রিয়)। মূলনীতি একই থাকে — সাবস্ক্রিপশনটি যেটির প্রকৃত দৃশ্যমান জীবনকালের সাথে বাঁধা, সেটির সাথেই বাঁধা উচিত, ব্লাইন্ডলি স্ক্রিনের লাইফসাইকেলের সাথে নয়।
-
পরীক্ষা করুন: দ্বিতীয় কোড সেলে ধাপ ৫-এর ঠিক পরে ইনবক্সকে আবার ফোরগ্রাউন্ডে ফেরান —
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 — সব এক জায়গায়।