Navigator 2.0 وواجهة Router
Navigator 2.0 وواجهة Router
نظام Navigator الأصلي في Flutter (الذي يُسمى أحياناً Navigator 1.0) عبارة عن مكدس أمري بسيط: تدفع المسارات وتسحبها. يعمل هذا النموذج بشكل جيد لمعظم التطبيقات، لكنه ينهار في اللحظة التي تحاول فيها المنصة المضيفة إخبار Flutter بما يجب عرضه—على سبيل المثال، عندما يتنقل مستخدم بعنوان URL مباشر إلى /products/42 على الويب، أو عندما يجب أن يسحب زر الرجوع في نظام التشغيل جزءاً فقط من مكدس متداخل.
يحل Navigator 2.0 (المعروف أيضاً بـ Router API) هذا الإشكال بجعل التنقل تصريحياً. بدلاً من دفع الصفحات بشكل أمري، تحتفظ بـ كائن حالة تطبيق يصف نية التنقل الحالية، ومجموعة من الفئات المتعاونة تستخلص مكدس الصفحات الصحيح من تلك الحالة—تلقائياً، في كل مرة تتغير فيها الحالة.
go_router وauto_route وbeamer. تعلم الواجهة البرمجية الخام يمنحك فهماً عميقاً لكيفية عمل تلك الحزم ومتى تلجأ إلى البدائيات مباشرة.القطع الأربعة المتعاونة
تتكون واجهة Router API من أربع فئات متفاعلة:
- RouteInformationProvider — يزود
RouteInformation(عنوان URI + كتلة حالة) من المنصة. التنفيذ الافتراضيPlatformRouteInformationProviderيستمع إلى شريط العنوان في المتصفح على الويب ونظام الـ intent في Android على الهاتف المحمول. - RouteInformationParser — يحول
RouteInformationالخام إلى كائن حالة التطبيق المُكتَّب لديك (مثلAppRoutePath). هنا يقع منطق تحليل عناوين URL. - RouterDelegate — قلب النظام. يحتفظ بحالة مسار التطبيق الحالية، ويبني ودجت
Navigatorمع قائمةpagesالصحيحة، ويُبلغ المنصة بعنوان URL الحالي عبرcurrentConfiguration. - BackButtonDispatcher — يعترض زر الرجوع للمنصة ويوزعه على المفوض النشط.
RootBackButtonDispatcherالافتراضي يعمل لمعظم الحالات.
تعريف حالة مسار تطبيقك
ابدأ بنمذجة كل ما يمكن لتطبيقك عرضه ككلاس Dart بسيط. اجعله غير قابل للتغيير وقابل للمقارنة.
نموذج مسار التطبيق
// يمثل كل "عنوان" يمكن للتطبيق أن يكون فيه
class AppRoutePath {
final bool isHomePage;
final int? productId; // غير فارغ عند الوجود في صفحة تفاصيل المنتج
final bool isUnknown;
const AppRoutePath.home()
: isHomePage = true,
productId = null,
isUnknown = false;
const AppRoutePath.productDetail(this.productId)
: isHomePage = false,
isUnknown = false;
const AppRoutePath.unknown()
: isHomePage = false,
productId = null,
isUnknown = true;
bool get isProductDetail => productId != null;
}
تطبيق RouteInformationParser
للمحلل مسؤوليتان: تحليل عنوان URL وارد إلى AppRoutePath، واستعادة AppRoutePath إلى RouteInformation ليبقى شريط المتصفح متزامناً.
AppRouteInformationParser
class AppRouteInformationParser
extends RouteInformationParser<AppRoutePath> {
@override
Future<AppRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.uri.toString());
// معالجة "/" => الصفحة الرئيسية
if (uri.pathSegments.isEmpty) {
return const AppRoutePath.home();
}
// معالجة "/products/:id" => صفحة تفاصيل المنتج
if (uri.pathSegments.length == 2 &&
uri.pathSegments[0] == 'products') {
final id = int.tryParse(uri.pathSegments[1]);
if (id != null) return AppRoutePath.productDetail(id);
}
return const AppRoutePath.unknown();
}
@override
RouteInformation restoreRouteInformation(AppRoutePath path) {
if (path.isHomePage) {
return RouteInformation(uri: Uri.parse('/'));
}
if (path.isProductDetail) {
return RouteInformation(
uri: Uri.parse('/products/${path.productId}'));
}
return RouteInformation(uri: Uri.parse('/404'));
}
}
تطبيق RouterDelegate
يمتد المفوض من RouterDelegate<AppRoutePath> ويمزج مع ChangeNotifier (حتى يستطيع ودجت Router الاستماع لتغييرات الحالة) وPopNavigatorRouterDelegateMixin (حتى يعمل زر الرجوع للنظام بشكل صحيح).
AppRouterDelegate — بناء مكدس الصفحات من الحالة
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// ---- حالة مسار التطبيق القابلة للتعديل ----
int? _selectedProductId;
bool _show404 = false;
// أدوات ضبط عامة تحدّث الحالة وتُخطر Router
void selectProduct(int id) {
_selectedProductId = id;
_show404 = false;
notifyListeners(); // يُطلق إعادة بناء Router
}
void clearSelection() {
_selectedProductId = null;
_show404 = false;
notifyListeners();
}
// ---- currentConfiguration: يخبر المنصة بعنوان URL الذي يُعرض ----
@override
AppRoutePath get currentConfiguration {
if (_show404) return const AppRoutePath.unknown();
if (_selectedProductId != null) {
return AppRoutePath.productDetail(_selectedProductId!);
}
return const AppRoutePath.home();
}
// ---- setNewRoutePath: يُستدعى عندما ترسل المنصة عنوان URL جديداً ----
@override
Future<void> setNewRoutePath(AppRoutePath path) async {
if (path.isUnknown) {
_selectedProductId = null;
_show404 = true;
} else if (path.isProductDetail) {
_selectedProductId = path.productId;
_show404 = false;
} else {
_selectedProductId = null;
_show404 = false;
}
// لا حاجة لـ notifyListeners() هنا — Router يستدعي build() تلقائياً
}
// ---- build: مكدس الصفحات هو دالة نقية للحالة ----
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
// الصفحة الرئيسية دائماً في المكدس
MaterialPage(
key: const ValueKey('HomePage'),
child: HomePage(
onProductSelected: selectProduct,
),
),
// صفحة التفاصيل مشروطة في الأعلى
if (_selectedProductId != null)
MaterialPage(
key: ValueKey('ProductDetailPage-$_selectedProductId'),
child: ProductDetailPage(
productId: _selectedProductId!,
onBack: clearSelection,
),
),
// صفحة 404 تعلو كل شيء
if (_show404)
const MaterialPage(
key: ValueKey('UnknownPage'),
child: UnknownPage(),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
clearSelection();
return true;
},
);
}
}
ربط كل شيء معاً في MaterialApp.router
مرر مفوضك ومحللك إلى MaterialApp.router. يصل الإطار RouteInformationProvider تلقائياً على الويب؛ على الهاتف المحمول يستخدم مزوداً بلا عملية فعلية.
main.dart
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _delegate = AppRouterDelegate();
final _parser = AppRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'عرض Navigator 2.0',
routerDelegate: _delegate,
routeInformationParser: _parser,
// اختياري: زود بمزودك الخاص للاختبار
// routeInformationProvider: ...,
);
}
}
RouterDelegate وRouteInformationParser في كائن State أو استخدم حل حقن تبعيات—لا تُنشئهما أبداً داخل build(). إعادة إنشائهما في كل إعادة بناء يُتلف حالة التنقل ويجعل التطبيق يعود إلى مسار الصفحة الرئيسية.أبرز مزايا Navigator 2.0 على Navigator 1.0
- دعم الروابط العميقة — يمكن للمنصة تسليم أي عنوان URL إلى
parseRouteInformationويُبنى مكدس الصفحات الصحيح تصريحياً. - تنقل الويب للأمام والخلف — تبقي
restoreRouteInformationشريط سجل المتصفح متزامناً. - قابلية الاختبار — مكدس الصفحات هو دالة نقية لـ
AppRoutePath؛ يمكنك اختبار منطق التوجيه بوحدات دون الحاجة إلىWidgetTester. - تحكم دقيق في زر الرجوع — المفوضون المتداخلون عبر
ChildBackButtonDispatcherيتيحون لـ Navigator الداخلية اعتراض زر الرجوع.
ملخص
يستبدل Navigator 2.0 نموذج الدفع/السحب الأمري بنموذج تصريحي: تُحدّث كائن الحالة؛ يعيد RouterDelegate بناء مكدس صفحات Navigator منه؛ ويحول RouteInformationParser عناوين URL إلى حالة والعكس. تعمل القطع الأربعة—RouteInformationProvider وRouteInformationParser وRouterDelegate وBackButtonDispatcher—معاً لمنح Flutter دعماً متكاملاً للروابط العميقة وسجل المتصفح. عادةً تستخدم التطبيقات الإنتاجية حزمة مثل go_router تُغلّف هذه البدائيات، لكن فهم الواجهة البرمجية الخام يجعلك مطور Flutter أكثر كفاءة بكثير.