التنقل والروابط العميقة باستخدام GoRouter
التنقل والروابط العميقة باستخدام GoRouter
GoRouter هو حزمة التوجيه التصريحية الموصى بها رسمياً من Flutter. تستبدل واجهة برمجة Navigator منخفضة المستوى بإعداد تصريحي قائم على عناوين URL يدعم الروابط العميقة وعناوين الويب والتنقل المتداخل لعلامات التبويب السفلية وحراسة المسارات — كل ذلك في نموذج متماسك واحد. في هذا الدرس ستُهيئ GoRouter من الصفر وتحمي المسارات بإعادة توجيه المصادقة وتوصّل التنقل المتداخل لشريط تبويبي وتتعامل مع الروابط العميقة القادمة من مصادر خارجية.
١. إعداد GoRouter
أضف التبعية وأنشئ نسخة واحدة من الموجّه، عادةً في ملف مستقل حتى يمكن استيراده في أي مكان دون تبعيات دائرية.
pubspec.yaml وإعداد الموجّه
# pubspec.yaml
dependencies:
go_router: ^13.2.0
# ─────────────────────────────────────────────
# lib/router/app_router.dart
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';
final GoRouter appRouter = GoRouter(
initialLocation: '/home',
routes: [
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'detail/:id', // يصبح /home/detail/:id
name: 'detail',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailScreen(itemId: id);
},
),
],
),
GoRoute(
path: '/profile',
name: 'profile',
builder: (context, state) => const ProfileScreen(),
),
],
);
مرّر الموجّه إلى MaterialApp.router بدلاً من MaterialApp العادي:
main.dart — توصيل GoRouter بالتطبيق
import 'package:flutter/material.dart';
import 'router/app_router.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'GoRouter Demo',
routerConfig: appRouter, // <— نسخة الموجّه الوحيدة
);
}
}
MaterialApp(home:) وMaterialApp(routes:). بمجرد التبديل إلى MaterialApp.router، يجب أن يمر كل تنقل عبر نسخة GoRouter — الجمع بين الأسلوبين يؤدي إلى تعارضات.٢. المسارات المسمّاة والمعاملات ذات الأنواع
استخدام name: على كل مسار يتيح لك التنقل دون ترميز سلاسل URL بشكل ثابت، مما يمنع الأخطاء المطبعية ويجعل إعادة الهيكلة آمنة.
context.goNamed('detail', pathParameters: {'id': '42'})— يستبدل إدخال المكدس الحاليcontext.pushNamed('detail', pathParameters: {'id': '42'})— يدفع فوق المكدسstate.pathParameters['id']— يقرأ مقطع URL المعلَن كـ:idstate.uri.queryParameters['q']— يقرأ سلسلة استعلام مثل?q=flutter
٣. حراسة المسارات — حماية المسارات التي تتطلب مصادقة
دالة الاسترجاع redirect الخاصة بـ GoRouter تعمل قبل كل حدث تنقل. إعادة مسار جديد تُعيد توجيه المستخدم؛ أما إعادة null فتسمح بالتنقل للمضي قُدُماً. أنظف نمط هو الاستماع إلى تدفق المصادقة عبر refreshListenable حتى تُعيد التغييرات في حالة تسجيل الدخول تقييم جميع عمليات إعادة التوجيه تلقائياً.
حارس المصادقة مع refreshListenable
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ChangeNotifier يلتف حول حالة المصادقة
class AuthNotifier extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
void login() { _isLoggedIn = true; notifyListeners(); }
void logout() { _isLoggedIn = false; notifyListeners(); }
}
final authNotifier = AuthNotifier();
// ── الموجّه مع إعادة توجيه عالمية ──────────────────────────────
final GoRouter appRouter = GoRouter(
initialLocation: '/home',
refreshListenable: authNotifier, // يُعيد تشغيل redirect عند تغيير المصادقة
redirect: (context, state) {
final loggedIn = authNotifier.isLoggedIn;
final onLoginPage = state.matchedLocation == '/login';
if (!loggedIn && !onLoginPage) return '/login';
if (loggedIn && onLoginPage) return '/home';
return null; // السماح بالتنقل
},
routes: [ /* ... */ ],
);
refreshListenable أي Listenable، لذا يمكنك تمرير ProviderListenable من Riverpod أو ValueNotifier أو دمج عدة مُبلِّغات بـ Listenable.merge([a, b]).٤. التنقل المتداخل مع ShellRoute (علامات التبويب السفلية)
يلف ShellRoute مجموعة من المسارات داخل ودجت صدفة دائمة — مثالي لأشرطة التنقل السفلية حيث تتشارك علامات التبويب هيكلاً مشتركاً لكن كل تبويب يحتفظ بمكدس التنقل الخاص به.
ShellRoute للتنقل التبويبي
import 'package:go_router/go_router.dart';
final GoRouter appRouter = GoRouter(
initialLocation: '/feed',
routes: [
ShellRoute(
builder: (context, state, child) => AppShell(child: child),
routes: [
GoRoute(
path: '/feed',
name: 'feed',
builder: (_, __) => const FeedScreen(),
routes: [
GoRoute(
path: 'post/:postId',
name: 'post',
builder: (context, state) => PostScreen(
postId: state.pathParameters['postId']!,
),
),
],
),
GoRoute(
path: '/search',
name: 'search',
builder: (_, __) => const SearchScreen(),
),
GoRoute(
path: '/profile',
name: 'profile',
builder: (_, __) => const ProfileScreen(),
),
],
),
],
);
// ودجت الصدفة التي تحتوي على BottomNavigationBar
class AppShell extends StatelessWidget {
final Widget child;
const AppShell({super.key, required this.child});
static const _tabs = ['/feed', '/search', '/profile'];
@override
Widget build(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
final index = _tabs.indexWhere((t) => location.startsWith(t));
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: index < 0 ? 0 : index,
onTap: (i) => context.go(_tabs[i]),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
٥. التعامل مع الروابط العميقة من مصادر خارجية
الروابط العميقة هي عناوين URL تفتح التطبيق وتنتقل مباشرةً إلى شاشة محددة — مثل إشعار دفع يفتح myapp://post/123، أو رابط ويب يشغّل بناء Flutter الويب على https://myapp.com/post/123. يتعامل GoRouter مع كليهما تلقائياً بمجرد تهيئة مخطط URL في ملفات المنصة.
- Android: أضف
<intent-filter>بالمخطط الخاص بك فيAndroidManifest.xml - iOS: أضف إدخال
CFBundleURLTypes(مخطط مخصص) أوAssociated Domains(روابط عالمية) فيInfo.plist - Flutter Web: يعمل فور الاستخدام — يقرأ GoRouter مسار URL المتصفح تلقائياً
/profile) والمستخدم غير مصادق، يجب أن تُعيد دالة الاسترجاع redirect التوجيه إلى /login وتحفظ المسار المطلوب أصلاً حتى تتمكن من إعادة التوجيه إليه بعد تسجيل الدخول. احفظه في معامل الاستعلام: أعد التوجيه إلى /login?redirect=/profile، ثم اقرأ state.uri.queryParameters['redirect'] بعد نجاح تسجيل الدخول.خلاصة
يجلب GoRouter التنقل التصريحي القائم على URL إلى Flutter. المفاهيم الأساسية هي: نسخة GoRouter وحيدة مع GoRoute مسمّاة، ودالة redirect عالمية مدعومة بـ refreshListenable لحراسة المصادقة، وShellRoute لصدفات التبويب السفلي الدائمة مع مكدسات متداخلة، ومعالجة روابط عميقة بدون إعداد يدوي بمجرد تسجيل مخططات URL للمنصة. هذه اللبنات الأساسية كافية لأي بنية تنقل على مستوى الإنتاج.