المسارات المتداخلة وShellRoute في GoRouter
المسارات المتداخلة وShellRoute في GoRouter
مع نمو تطبيقات Flutter، تحتاج في الغالب إلى غلاف (shell) ثابت — شريط تنقل سفلي أو درج أو شريط جانبي — يبقى مثبتاً على الشاشة بينما ينتقل المستخدم بين الصفحات الفرعية المختلفة. ShellRoute في GoRouter هو الآلية المصممة خصيصاً لهذا النمط. على عكس GoRoute البسيط الذي يستبدل مكدس الصفحات بأكمله، تلف ShellRoute مساراتها الفرعية داخل منشئ Widget مشترك، فيبقى الغلاف حياً ويتم تبديل المسار الفرعي النشط في مكانه.
لماذا وُجدت ShellRoute
بدون ShellRoute، كل عملية تنقل بعمق تدمر وتعيد إنشاء السقالة (scaffold) التي تحتوي على BottomNavigationBar، مما يسبب وميضاً مرئياً وفقدان موضع التمرير أو حالة النموذج لكل تبويب. تحل ShellRoute هذه المشكلة بتثبيت ودجت الغلاف مرة واحدة والسماح للمسارات الفرعية بالعرض داخل ودجت Navigator تعيش داخل جسم الغلاف. النتيجة مطابقة لما ينتجه نمط IndexedStack الكلاسيكي في Flutter، لكنه يعمل مع نظام التنقل القائم على URL في GoRouter.
ShellRoute في الإصدار 6.x من go_router. وهو الاستبدال الموصى به لبناء التخطيطات القائمة على التبويبات أو الأدراج. افضل دائماً استخدامه على الإدارة اليدوية لـ IndexedStack عند استخدام GoRouter بالفعل.تشريح ShellRoute
تتطلب ShellRoute builder (أو pageBuilder) يستقبل BuildContext وGoRouterState وWidget child. الـ child هو المسار الفرعي النشط حالياً الذي يعرضه GoRouter. تضعه داخل سقالة الغلاف الخاصة بك في أي مكان يجب أن يظهر المحتوى فيه.
إعداد ShellRoute الأساسي
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final GoRouter appRouter = GoRouter(
initialLocation: '/home',
routes: [
ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/search',
builder: (context, state) => const SearchScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
),
],
);
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex(context),
onTap: (index) => _onTap(context, index),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'الرئيسية'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'بحث'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'الملف'),
],
),
);
}
int _selectedIndex(BuildContext context) {
final String location = GoRouterState.of(context).uri.toString();
if (location.startsWith('/search')) return 1;
if (location.startsWith('/profile')) return 2;
return 0;
}
void _onTap(BuildContext context, int index) {
switch (index) {
case 0: context.go('/home'); break;
case 1: context.go('/search'); break;
case 2: context.go('/profile'); break;
}
}
}
GoRouterState.of(context).uri.toString() (GoRouter 10+) أو GoRouterState.of(context).location (الإصدارات الأقدم) داخل الغلاف لتحديد التبويب النشط. يبقي هذا الغلاف عديم الحالة — لا حاجة لـ setState لتمييز التبويب.التداخل العميق: المسارات الفرعية داخل ShellRoute
يمكنك تداخل GoRoutes على مستويات متعددة تحت ShellRoute. على سبيل المثال، قد يحتوي تبويب /profile على صفحة فرعية /profile/edit تعرض شريط التنقل السفلي. أضفها كفرع لمسار /profile:
المسارات الفرعية المتداخلة تحت ShellRoute
ShellRoute(
builder: (context, state, child) => ScaffoldWithNavBar(child: child),
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
routes: [
// هذا المسار الفرعي لا يزال داخل الغلاف — شريط التنقل يبقى ظاهراً
GoRoute(
path: 'edit', // يُحسم إلى /profile/edit
builder: (context, state) => const EditProfileScreen(),
),
],
),
],
),
// خارج ShellRoute — بدون شريط تنقل (مثل صفحة تفاصيل بملء الشاشة)
GoRoute(
path: '/product/:id',
builder: (context, state) {
final String id = state.pathParameters['id']!;
return ProductDetailScreen(productId: id);
},
),
الحفاظ على الحالة عبر التبويبات
بشكل افتراضي، يؤدي التنقل بعيداً عن تبويب باستخدام context.go() إلى التخلص من ودجت التبويب السابق. للحفاظ على موضع التمرير والحالة الداخلية لكل تبويب، استخدم StatefulShellRoute مع StatefulShellBranch (مُقدَّم في GoRouter 9.x). يحصل كل فرع على Navigator خاص به، مما يبقي شجرة الودجت الخاصة به حية في الذاكرة.
StatefulShellRoute.indexedStack هو المُنشئ الأكثر شيوعاً. يعكس نمط IndexedStack الكلاسيكي لكنه يتكامل بالكامل مع نظام URL في GoRouter.مثال على StatefulShellRoute
استخدم StatefulShellRoute.indexedStack عندما يجب أن يحتفظ كل تبويب بمكدس التنقل وحالة الودجت الخاصة به بشكل مستقل:
StatefulShellRoute.indexedStack
final GoRouter appRouter = GoRouter(
initialLocation: '/feed',
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/feed',
builder: (context, state) => const FeedScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/explore',
builder: (context, state) => const ExploreScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/inbox',
builder: (context, state) => const InboxScreen(),
),
],
),
],
),
],
);
class ScaffoldWithNavBar extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const ScaffoldWithNavBar({super.key, required this.navigationShell});
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell, // يعرض الفرع النشط
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex,
onTap: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.rss_feed), label: 'الخلاصة'),
BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'استكشاف'),
BottomNavigationBarItem(icon: Icon(Icons.inbox), label: 'البريد'),
],
),
);
}
}
ShellRoute داخل ShellRoute أخرى إلا إذا كان لديك سبب محدد للأغلفة المتداخلة (مثل قسم فرعي له شريط تبويبات خاص). الأغلفة المتداخلة بعمق تجعل مسارات URL صعبة الفهم وقد تخلق سلوكاً محيراً لزر الرجوع.مقارنة سريعة: ShellRoute مقابل StatefulShellRoute
- ShellRoute — غلاف بسيط. المسارات الفرعية تشترك في نفس
Navigator. التنقل بعيداً يتخلص من الفرع السابق. استخدمه عندما لا تحتاج التبويبات إلى حالة مستقلة. - StatefulShellRoute — كل فرع له
Navigatorخاص. التبديل بين التبويبات يحافظ على الحالة. استخدمه عندما تحتاج إلى الحفاظ على حالة التبويب (مواضع التمرير، البيانات المحملة، إلخ).
ملخص
ShellRoute هي حجر الأساس لتنقل الغلاف الثابت في GoRouter. تتيح لك تثبيت سقالة مرة واحدة وتبديل المسارات الفرعية بداخلها، مما يتيح أشرطة التنقل السفلية والأدراج بدون إعادة بناء الصفحات. للتطبيقات التي تحتاج إلى الحفاظ على حالة كل تبويب، انتقل إلى StatefulShellRoute.indexedStack. كلا النهجين يبقيان شجرة المسارات تصريحية ومدفوعة بالURL ومتوافقة مع الروابط العميقة — وهي سمات بنية تنقل Flutter المنظمة جيداً.
ShellRoute على المستوى الأعلى من قائمة GoRouter.routes، وليس كفروع لها.