التنقل والتوجيه

التنقل بالتبويبات باستخدام BottomNavigationBar

15 دقيقة الدرس 5 من 14

التنقل بالتبويبات باستخدام BottomNavigationBar

تُقدّم معظم تطبيقات الجوّال شريطاً سفلياً ثابتاً يتيح للمستخدمين القفز بين أقسام التطبيق الرئيسية بنقرة واحدة. في Flutter، يُعدّ BottomNavigationBar الودجت الرسمي لهذا النمط. ومع استخدامه مقروناً بـ IndexedStack، يظل كل تبويب حياً في الذاكرة، فتُحفظ مواضع التمرير وحقول الإدخال والبيانات المحمّلة عند التنقل بين التبويبات — وهو السلوك الذي يتوقعه المستخدمون من تطبيق متقن.

كيف يعمل BottomNavigationBar

يُوضع BottomNavigationBar في فتحة bottomNavigationBar داخل Scaffold. يحمل قائمة من BottomNavigationBarItem، كلٌّ منها يحوي أيقونة وتسمية. لا يتولّى الشريط التنقل بنفسه؛ بل يُبلّغ عن الفهرس الذي نقر عليه المستخدم عبر دالة ردّ الفعل onTap. تحتفظ StatefulWidget بالفهرس الحالي في الحالة، وتستخدمه لتحديد الشاشة التي تُعرض في الجسم.

ملاحظة: يتطلب BottomNavigationBar عنصرَين على الأقل. عند وجود أكثر من ثلاثة، استخدم BottomNavigationBarType.fixed لإبقاء جميع التسميات ظاهرة. النوع الافتراضي هو shifting الذي يُحرّك التسميات ويخفي غير المحدد منها.

التبديل الساذج للجسم مقابل IndexedStack

أبسط نهج هو استخدام تعبير شرطي في الجسم يُرجع ودجتاً مختلفاً لكل فهرس. هذا يصلح للشاشات الثابتة، لكنه يدمّر ويُعيد بناء كل شاشة في كل مرة يُبدّل المستخدم التبويب، مما يُفقد الحالة المحلية كلها (موضع التمرير، البيانات المحمّلة، نص الإدخال، إلخ).

تحلّ IndexedStack هذه المشكلة بأناقة. تبني جميع الأبناء دفعةً واحدة وتُبقيهم أحياء في الذاكرة، لكنها تُظهر فقط الابن الموجود عند الفهرس index. تبديل التبويبات لا يفعل أكثر من تغيير أيّ الأبناء مرئيّ — والباقون يظلون سليمين.

المثال الأول — BottomNavigationBar أساسي مع IndexedStack

import 'package:flutter/material.dart';

class MainShell extends StatefulWidget {
  const MainShell({super.key});

  @override
  State<MainShell> createState() => _MainShellState();
}

class _MainShellState extends State<MainShell> {
  int _selectedIndex = 0;

  // جميع الشاشات الرئيسية تُعلَن مرةً واحدة وتظل حية
  static const List<Widget> _screens = [
    HomeScreen(),
    SearchScreen(),
    ProfileScreen(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: _screens,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home_outlined),
            activeIcon: Icon(Icons.home),
            label: 'الرئيسية',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search_outlined),
            activeIcon: Icon(Icons.search),
            label: 'بحث',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person_outline),
            activeIcon: Icon(Icons.person),
            label: 'الملف الشخصي',
          ),
        ],
      ),
    );
  }
}
نصيحة: أعلن قائمة _screens بوصفها static const. هذا يضمن إنشاء ودجات الشاشة مرةً واحدة فحسب طوال عمر كائن الحالة — لا تُعاد الإنشاء في كل استدعاء لـ setState. إن احتاجت الشاشات معاملات في مُنشئها، فهيّئ القائمة في initState() بدلاً من ذلك.

تنسيق الشريط

يمكنك تخصيص كل جانب بصري للشريط دون مغادرة مُنشئ الودجت نفسه:

  • selectedItemColor وunselectedItemColor — ألوان الأيقونة والتسمية.
  • backgroundColor — خلفية الشريط.
  • selectedFontSize / unselectedFontSize — أحجام التسميات.
  • elevation — عمق الظل فوق الشريط.
  • type: BottomNavigationBarType.fixed — إبقاء جميع التسميات ظاهرة (مطلوب لأربعة عناصر أو أكثر).

الحفاظ على الحالة داخل التبويبات

بما أن IndexedStack تُبقي كل ودجت ابن في شجرة الودجات، تحتفظ أي StatefulWidget متداخلة داخل تبويب بحالتها تلقائياً. ومع ذلك، إن أُعيد بناء ودجت التبويب (على سبيل المثال لأنه يتلقى بيانات جديدة من مجرى)، فإن شجرة ذلك التبويب فحسب هي التي تُعاد رسمها — التبويبات الأخرى لا تتأثر.

تحذير: لا تُنشئ كائنات ودجات داخل قائمة children الخاصة بـ IndexedStack أثناء تنفيذ دالة build إن كانت تلك الودجات تحمل حالة ثقيلة. كل استدعاء لـ build سيُنشئ كائنات ودجات جديدة (رغم أن Flutter يُطابقها عبر المفاتيح). نمط القائمة الثابتة الموضّح أعلاه هو النهج الصحيح.

المثال الثاني — تطبيق بأربعة تبويبات مع تنسيق وعداد الشارة

import 'package:flutter/material.dart';

class AppShell extends StatefulWidget {
  const AppShell({super.key});

  @override
  State<AppShell> createState() => _AppShellState();
}

class _AppShellState extends State<AppShell> {
  int _currentIndex = 0;
  int _notificationCount = 3; // عدد الإشعارات المحاكى

  late final List<Widget> _pages;

  @override
  void initState() {
    super.initState();
    _pages = [
      const DashboardPage(),
      const ExplorePage(),
      const NotificationsPage(),
      const SettingsPage(),
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) => setState(() => _currentIndex = index),
        type: BottomNavigationBarType.fixed,
        selectedItemColor: Theme.of(context).colorScheme.primary,
        unselectedItemColor: Colors.grey,
        elevation: 8,
        items: [
          const BottomNavigationBarItem(
            icon: Icon(Icons.dashboard_outlined),
            activeIcon: Icon(Icons.dashboard),
            label: 'لوحة التحكم',
          ),
          const BottomNavigationBarItem(
            icon: Icon(Icons.explore_outlined),
            activeIcon: Icon(Icons.explore),
            label: 'استكشاف',
          ),
          BottomNavigationBarItem(
            icon: Badge(
              label: Text('$_notificationCount'),
              isLabelVisible: _notificationCount > 0,
              child: const Icon(Icons.notifications_outlined),
            ),
            activeIcon: Badge(
              label: Text('$_notificationCount'),
              isLabelVisible: _notificationCount > 0,
              child: const Icon(Icons.notifications),
            ),
            label: 'تنبيهات',
          ),
          const BottomNavigationBarItem(
            icon: Icon(Icons.settings_outlined),
            activeIcon: Icon(Icons.settings),
            label: 'الإعدادات',
          ),
        ],
      ),
    );
  }
}

متى تستخدم NavigationBar بدلاً من ذلك

قدّم Flutter 3 ودجت NavigationBar (بتصميم Material 3) بوصفه الخلف الحديث لـ BottomNavigationBar. يتبع لغة تصميم Material You مع مؤشرات بشكل حبّة الدواء ومساحات لمس أكبر. الواجهة البرمجية شبه متطابقة — فقط استبدل اسم الودجت واستعض عن BottomNavigationBarItem بـ NavigationDestination. كلا الودجتين يتكاملان بسلاسة مع IndexedStack.

ملخص

التنقل بالتبويبات في Flutter مباشر: تتتبع StatefulWidget الفهرس المحدد، ويُبلّغ BottomNavigationBar (أو NavigationBar) عن النقرات، وتُصيّر IndexedStack جميع أجسام التبويبات مع الحفاظ على حالتها حية. هذا التوليف من ثلاثة ودجات هو أساس كل تطبيق Flutter قائم على التبويبات تقريباً.

النقطة الرئيسية: آثِر دائماً IndexedStack على التبديل الساذج للجسم حين تحوي تبويباتك ودجات ذات حالة. تكلفة الأداء المترتبة على إبقاء الودجات غير المرئية حيةً تافهة مقارنةً بمكسب تجربة المستخدم المتمثّل في حفظ مواضع التمرير والبيانات المحمّلة.