التنقّل والتوجيه بمرونة Navigation & Adaptive Overlays
كيف يتحرك المستخدم بين الشاشات، وكيف نصمم واجهات تتغير بناءً على حجم الشاشة. How users move between screens and how to design UI that adapts to screen sizes.
التطبيق مش دايماً بيفتح على الـ Dashboard. فيه "بوابات" لازم المستخدم يعدي عليها بالترتيب:
لو دي أول مرة يفتح التطبيق، لازم يوافق على سياسة الخصوصية.
لو مفعل البصمة، لازم يتأكد من هويته قبل ما يشوف البيانات.
أخيراً يوصل للشاشة الرئيسية.
home: prefs.isPrivacyAccepted
? (settings.isBiometricEnabled
? const AuthGuardScreen()
: const MainScreen())
: const PrivacyConsentScreen(),
في الـ MRE CashBook، إحنا مهتمين جداً إن شكل القوائم (Dialogs/Sheets) يكون مريح. على الموبايل بتظهر من تحت (Bottom Sheet)، وعلى التابلت بتظهر في نص الشاشة (Dialog).
static Future showAdaptiveModal(BuildContext context, {required Widget child}) {
if (ResponsiveLayout.isMobile(context)) {
return _showMobileSheet(context, child: child);
}
return _showDesktopDialog(context, child: child);
}
هنا الكود "بيشم" حجم الشاشة. لو لقى العرض صغير، بيشغل showModalBottomSheet. لو العرض كبير، بيشغل showGeneralDialog مع أنيميشن احترافي.
لما بننتقل لشاشة تفاصيل كتاب معين، لازم نبعت معانا "الشنطة" اللي فيها بيانات الكتاب ده. إزاي بنعمل كدة في الـ 1.0؟
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookDetailsScreen(book: currentBook),
settings: const RouteSettings(name: '/book-details'),
),
);
لاحظ إننا استخدمنا الـ Constructor بتاع الشاشة مباشرة. دي أسهل وأأمن طريقة (Type-Safe) عشان تتأكد إن الشاشة استلمت الـ Object الصح.
إحنا كمان بنسمي الـ Route في RouteSettings عشان لو حد حب يتتبع المستخدم (Analytics) يعرف هو واقف في أنهي شاشة.
تخيل إنت بتكتب عملية حسابية طويلة، وفجأة دوست "رجوع" بالغلط. في MRE CashBook، بنستخدم PopScope (البديل الحديث لـ WillPopScope) عشان نمنع الكارثة دي.
return PopScope(
canPop: false, // بنقفل الرجوع التلقائي
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldPop = await _showConfirmExitDialog();
if (shouldPop && context.mounted) {
Navigator.of(context).pop();
}
},
child: Scaffold(...),
);
دي حركة احترافية جداً بتخلي المستخدم يتأكد إنه مش هيخسر بياناته اللي لسه معملهاش Save.
أحياناً إنت بتبعت المستخدم يختار حاجة من شاشة تانية (زي الـ Date Picker أو الـ Category). لما بيرجع، لازم يرجع ومعاه "الهدية".
// في الشاشة اللي بنختار منها
Navigator.pop(context, selectedCategory);
// في الشاشة الأصلية اللي مستنية
final result = await Navigator.push(context, route);
if (result != null) {
_updateUI(result);
}
ده بيخلي التواصل بين الشاشات شغال في الاتجاهين (Two-way Communication).
التنقل الناشف بيخلي التطبيق يحسسك إنه "قديم". إحنا بنستخدم الـ Hero عشان البيانات "تطير" من شاشة للتانية.
// في شاشة القائمة
Hero(
tag: 'book-${book.id}',
child: BookIcon(book),
);
// في شاشة التفاصيل
Hero(
tag: 'book-${book.id}',
child: LargeBookIcon(book),
);
بمجرد ما تستخدم نفس الـ tag، فلاتر بيكلف نفسه إنه يعمل أنيميشن ناعم جداً يحسس المستخدم إن الشاشة بتكبر مش بتبدل.
التنقل السلس والواجهة المرنة هما الفرق بين تطبيق "شغال" وتطبيق "احترافي".