পাঠ ২৫ · ৫৭-এর মধ্যে · মডিউল ৬
Home / Courses / Mobile App Development / ট্যাব ও ড্রয়ার

ট্যাব ও ড্রয়ার নেভিগেশন

Tab & drawer navigation
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ট্যাব নেভিগেশন কেন একটিমাত্র শেয়ার্ড স্ট্যাক দিয়ে কাজ করে না — কেন প্রতিটি ট্যাবের নিজস্ব স্ট্যাক দরকার
  • dict-এ রাখা একাধিক স্বাধীন NavigationStack ইনস্ট্যান্স দিয়ে ট্যাব নেভিগেশন বাস্তবায়ন
  • ট্যাব সুইচ করার পরও প্রতিটি ট্যাবের হিস্ট্রি অক্ষত থাকে তা assert দিয়ে যাচাই
  • ড্রয়ার নেভিগেশনের হালকা সিমুলেশন — টগল বুলিয়ান + আইটেম-থেকে-রুট-স্ক্রিন নির্বাচন

১ · ট্যাব নেভিগেশন কী — এবং কেন প্রতিটি ট্যাবের নিজস্ব স্ট্যাক দরকার

নিচের ট্যাব বার (Home / Search / Profile) সহ প্রায় প্রতিটি জনপ্রিয় অ্যাপে দেখা মূল ধারণাটি হলো — প্রতিটি ট্যাব আসলে তার নিজস্ব, সম্পূর্ণ আলাদা L24-এর NavigationStack। আপনি Home ট্যাবে গভীরে (Home → PostDetail → CommentThread) নেভিগেট করে Search ট্যাবে সুইচ করলে, Home ট্যাবের হিস্ট্রি হারিয়ে যায় না — এটি ঠিক যেখানে ছিল সেখানেই "জমা" থাকে, এবং আপনি আবার Home ট্যাবে ফিরে এলে ঠিক সেই CommentThread স্ক্রিনটিই দেখতে পান। যদি সবগুলো ট্যাব একটিমাত্র শেয়ার্ড স্ট্যাক ব্যবহার করতো, তাহলে ট্যাব সুইচ করাটাই আসলে একটি push()/pop() এর মতো আচরণ করতো এবং একটি ট্যাবের ব্যাক বাটন ভুলবশত অন্য ট্যাবের স্ক্রিন সরিয়ে দিতে পারতো — তাই বাস্তব ফ্রেমওয়ার্কগুলো (React Navigation-এর Tab Navigator, Jetpack Compose-এর NavHost প্রতি-ট্যাব, iOS-এর UITabBarController) প্রতিটি ট্যাবের জন্য আলাদা নেভিগেশন কনটেইনার রাখে।

Home ট্যাব Search ট্যাব Profile ট্যাব HomeFeed PostDetail CommentThread (top) SearchRoot SearchResults (top) ProfileRoot (top)
তিনটি ট্যাবের গভীরতা ভিন্ন (৩, ২, ১) — প্রতিটি সম্পূর্ণ স্বাধীন NavigationStack, একে অপরকে প্রভাবিত করে না।

২ · কোড: dict-এ রাখা একাধিক স্বাধীন স্ট্যাক

নিচের কোডে তিনটি ট্যাবের জন্য তিনটি সম্পূর্ণ আলাদা NavigationStack ইনস্ট্যান্স তৈরি করা হয়েছে, একটি dict-এ ট্যাবের নাম দিয়ে কী (key) করে রাখা। Home ট্যাবে দুইবার push() করার পর Search ট্যাবে সুইচ করে সেখানে একবার push() করা হয়েছে, তারপর আবার Home ট্যাবে ফিরে এসে যাচাই করা হয়েছে যে Home-এর হিস্ট্রি অক্ষত আছে কিনা।

Python
# L24-এর NavigationStack ক্লাসটি এই পাতায় নতুন করে সংজ্ঞায়িত করা হলো
# (প্রতিটি লেসন পেজ একটি আলাদা Pyodide সেশন, তাই ফাইল জুড়ে import সম্ভব নয়)

class NavigationStack:
    def __init__(self, root_screen):
        self.stack = [root_screen]

    def push(self, screen):
        self.stack.append(screen)

    def pop(self):
        if len(self.stack) <= 1:
            return None
        return self.stack.pop()

    def current(self):
        return self.stack[-1]


# প্রতিটি ট্যাবের নিজস্ব, সম্পূর্ণ স্বাধীন নেভিগেশন স্ট্যাক -- একটি dict-এ রাখা হলো
tabs = {
    "Home":    NavigationStack("HomeFeed"),
    "Search":  NavigationStack("SearchRoot"),
    "Profile": NavigationStack("ProfileRoot"),
}
active_tab = "Home"

def switch_tab(name):
    global active_tab
    active_tab = name
    print(f"সক্রিয় ট্যাব -> {name}  (এই ট্যাবের current স্ক্রিন: {tabs[name].current()})")


print(f"শুরু -- active_tab: {active_tab}")

tabs["Home"].push("PostDetail")
tabs["Home"].push("CommentThread")
print(f"Home ট্যাবে দুইবার push করার পর -> {tabs['Home'].stack}")

switch_tab("Search")
tabs["Search"].push("SearchResults")
print(f"Search ট্যাবে push করার পর  -> {tabs['Search'].stack}")

switch_tab("Home")
print(f"Home ট্যাবে ফিরে আসার পর    -> {tabs['Home'].stack}")

assert tabs["Home"].stack == ["HomeFeed", "PostDetail", "CommentThread"]
assert tabs["Search"].stack == ["SearchRoot", "SearchResults"]
assert tabs["Profile"].stack == ["ProfileRoot"]
print("\nযাচাই সফল: ট্যাব সুইচ করলেও প্রতিটি ট্যাবের নিজস্ব নেভিগেশন ইতিহাস অক্ষত থাকে।")

    
লক্ষ্য করুন switch_tab() কখনো কোনো স্ট্যাক মিউটেট করে না — এটি শুধু active_tab ভ্যারিয়েবলটি বদলায় এবং সেই ট্যাবের বর্তমান current() রিপোর্ট করে। Profile ট্যাবের স্ট্যাক পুরো সেলে একবারও স্পর্শ করা হয়নি, তাই শেষে সেটি এখনও ঠিক ['ProfileRoot'] — তিনটি assert লাইনই এটি নিশ্চিত করে (কোনোটি ব্যর্থ হলে সেল একটি AssertionError দেখিয়ে থেমে যেত)।

৩ · ড্রয়ার নেভিগেশন — একটি হালকা সিমুলেশন

ড্রয়ার (হ্যামবার্গার মেনু থেকে খোলা সাইড-প্যানেল) সাধারণত একই সময়ে একাধিক স্বাধীন হিস্ট্রি ট্র্যাক করার দরকার হয় না — এটি মূলত একটি টগল (খোলা/বন্ধ) আর একটি তালিকা থেকে একটি রুট স্ক্রিন নির্বাচন। তাই এর সিমুলেশন ট্যাব নেভিগেশনের চেয়ে অনেক হালকা — একটি বুলিয়ান ভ্যারিয়েবল এবং একটি ফাংশন যথেষ্ট।

Python
# ড্রয়ার নেভিগেশন -- একটি টগল বুলিয়ান + ড্রয়ার-আইটেম অনুযায়ী রুট স্ক্রিন নির্বাচন।
# (এই সেলটি আগের সেলে সংজ্ঞায়িত NavigationStack পুনরায় ব্যবহার করছে)

drawer_open = False

DRAWER_ITEMS = {
    "ড্যাশবোর্ড": "DashboardRoot",
    "সেটিংস":     "SettingsRoot",
    "লগআউট":     "LogoutConfirm",
}

def toggle_drawer():
    global drawer_open
    drawer_open = not drawer_open
    print(f"ড্রয়ার {'খোলা' if drawer_open else 'বন্ধ'} হলো")

def select_drawer_item(item_name):
    global drawer_open
    if item_name not in DRAWER_ITEMS:
        print(f"অজানা ড্রয়ার আইটেম: {item_name!r}")
        return None
    root_screen = DRAWER_ITEMS[item_name]
    drawer_open = False   # আইটেম বেছে নেওয়ার পর ড্রয়ার নিজে থেকে বন্ধ হয়ে যায়
    print(f"'{item_name}' নির্বাচিত -> নতুন রুট স্ক্রিন: {root_screen}  (ড্রয়ার বন্ধ)")
    return root_screen


toggle_drawer()
chosen_root = select_drawer_item("সেটিংস")

new_stack = NavigationStack(chosen_root)
print(f"\nএই ড্রয়ার-আইটেমের জন্য নতুন নেভিগেশন স্ট্যাক: {new_stack.stack}")
print(f"ড্রয়ার এখন খোলা আছে? {drawer_open}")

    
select_drawer_item() একটি নতুন রুট স্ক্রিনের নাম ফেরত দেয় — এটি নিয়ে একটি সম্পূর্ণ নতুন NavigationStack তৈরি করা হয়েছে, কারণ ড্রয়ার থেকে একটি আইটেম বেছে নেওয়া মানে সাধারণত পুরনো স্ক্রিন-হিস্ট্রি বাতিল করে একটি সম্পূর্ণ নতুন বিভাগে (section) প্রবেশ করা — যেমন "ড্যাশবোর্ড" থেকে "সেটিংসে" গেলে ড্যাশবোর্ডের গভীর নেভিগেশন হিস্ট্রি রাখার প্রয়োজন সাধারণত থাকে না।
মূল কথা · Key takeaway

ট্যাব নেভিগেশন = একাধিক স্বাধীন NavigationStack, একটি dict-এ ট্যাবের নাম দিয়ে সংগঠিত — ট্যাব সুইচ করাটা কোনো স্ট্যাক মিউটেট করে না। ড্রয়ার নেভিগেশন অনেক সরল — একটি খোলা/বন্ধ টগল এবং আইটেম-থেকে-রুট-স্ক্রিন ম্যাপিং। দুটোই L24-এর একই NavigationStack বিল্ডিং ব্লক ব্যবহার করে, শুধু ভিন্নভাবে সংগঠিত।

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

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

প্র ০১ যদি সবগুলো ট্যাব একটিমাত্র শেয়ার্ড NavigationStack ব্যবহার করতো (আলাদা dict এন্ট্রির বদলে), তাহলে কী সমস্যা হতো?

ট্যাব সুইচ করাটাই একটি push()-এর মতো আচরণ করতো, আর একটি ট্যাবের ব্যাক বাটনে চাপলে সেটি ভুলবশত অন্য ট্যাবের স্ক্রিন pop() করে ফেলতে পারতো। প্রতিটি ট্যাবের হিস্ট্রি মিশে যেত — ব্যবহারকারী Search ট্যাব থেকে ব্যাক করলে হয়তো Home ট্যাবের কোনো স্ক্রিনে চলে যেতেন, যা ব্যবহারকারীর প্রত্যাশার সম্পূর্ণ বিপরীত।

প্র ০২ ড্রয়ার নেভিগেশনের সিমুলেশন ট্যাব নেভিগেশনের চেয়ে অনেক হালকা — এর মানে কি ড্রয়ার কম গুরুত্বপূর্ণ?

না, শুধু কাঠামোগতভাবে সরল। ড্রয়ার একবারে একটিমাত্র সক্রিয় বিভাগ দেখায় — একাধিক সমান্তরাল হিস্ট্রি একসাথে ট্র্যাক করার দরকার হয় না। বাস্তবে অনেক অ্যাপ ড্রয়ার আইটেম নির্বাচনকে ট্যাবের মতোই dict-of-stacks প্যাটার্নের সাথে যুক্ত করে (প্রতিটি ড্রয়ার-বিভাগেরও নিজস্ব স্ট্যাক), তখন এটি ট্যাব নেভিগেশনেরই একটি ভিন্ন প্রবেশপথ (entry point) হয়ে ওঠে।

প্র ০৩ উপরের কোডে switch_tab("Search") কল করলে কি Search ট্যাবের স্ট্যাকে কোনো পরিবর্তন হয়?

না। switch_tab() শুধু active_tab ভ্যারিয়েবল বদলায় এবং সেই ট্যাবের বর্তমান current() স্ক্রিন রিপোর্ট করে — এটি কোনো push()/pop() কল করে না। স্ট্যাক মিউটেট হয় শুধুমাত্র যখন কেউ সরাসরি সেই ট্যাবের স্ট্যাক অবজেক্টে push()/pop() কল করে, যেমন এরপরের লাইনে tabs["Search"].push("SearchResults")।

অনুশীলন

  1. চিন্তা করুন: একটি সোশ্যাল মিডিয়া অ্যাপে তিনটি ট্যাব — Home, Notifications, Profile। ব্যবহারকারী Profile ট্যাবে গভীরে যান (Profile → EditProfile → ChangePassword), তারপর Home ট্যাবে সুইচ করে কিছুক্ষণ স্ক্রল করেন, তারপর আবার Profile ট্যাবে ফিরে আসেন। dict-of-stacks ডিজাইন অনুযায়ী তখন কোন স্ক্রিন দেখা উচিত, এবং কেন?

    ঠিক সেই ChangePassword স্ক্রিনটিই দেখা উচিত — Profile ট্যাবের স্ট্যাক ["Profile", "EditProfile", "ChangePassword"] অবস্থাতেই ছিল, কারণ Home ট্যাবে সুইচ করা বা সেখানে স্ক্রল করা Profile ট্যাবের স্বাধীন স্ট্যাককে একেবারেই স্পর্শ করেনি। এটিই dict-of-stacks ডিজাইনের মূল লক্ষ্য — প্রতিটি ট্যাবের নেভিগেশন হিস্ট্রি সম্পূর্ণ বিচ্ছিন্নভাবে সংরক্ষিত থাকে।

  2. পরীক্ষা করুন: উপরের প্রথম কোড সেলে tabs dict-এ একটি চতুর্থ ট্যাব "Notifications" যোগ করুন (রুট স্ক্রিন "NotificationsRoot"), তারপর সেই ট্যাবে push("NotificationDetail") করুন, এবং শেষে একটি নতুন assert লাইন যোগ করুন যা নিশ্চিত করে Profile ট্যাব পুরো সময় অস্পৃশ্য (untouched) ছিল।

    tabs["Notifications"] = NavigationStack("NotificationsRoot") যোগ করার পর tabs["Notifications"].push("NotificationDetail") কল করলে সেই ট্যাবের স্ট্যাক ["NotificationsRoot", "NotificationDetail"] হবে। এরপর assert tabs["Profile"].stack == ["ProfileRoot"] যোগ করলে সেটি পাস করবে — প্রমাণ করে চতুর্থ ট্যাব যোগ করা বা তাতে নেভিগেট করা বাকি ট্যাবগুলোর কোনোটিকেই প্রভাবিত করেনি।

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

আগের পাঠ
স্ট্যাক-ভিত্তিক নেভিগেশন