ناوبری و Routing
ناوبری بین صفحات یکی از مهمترین قسمتهای هر اپلیکیشن است. Flutter دو سیستم ناوبری دارد: Navigator 1.0 (ساده) و Navigator 2.0 (declarative). همچنین package های مدرنی مثل go_router کار را راحتتر میکنند.
۵.۱ مقدمه
ناوبری بین صفحات یکی از مهمترین قسمتهای هر اپلیکیشن است. Flutter دو سیستم ناوبری دارد: Navigator 1.0 (ساده) و Navigator 2.0 (declarative). همچنین package های مدرنی مثل go_router کار را راحتتر میکنند.
۵.۲ Navigator 1.0 — ساده و سریع
// رفتن به صفحه جدید
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProductDetailPage(),
),
);
// برگشت
Navigator.pop(context);
// با تأیید قبل از pop
final shouldPop = await showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("خروج؟"),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("خیر")),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text("بله")),
],
),
);
if (shouldPop == true && context.mounted) {
Navigator.pop(context);
}
// pushReplacement (جایگزین صفحه فعلی)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
);
// pushAndRemoveUntil (clear stack)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false, // حذف همه
);
Pass Data به صفحه جدید
// روش ۱: Constructor
class ProductDetailPage extends StatelessWidget {
final Product product;
const ProductDetailPage({super.key, required this.product});
// ...
}
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProductDetailPage(product: product),
),
);
// روش ۲: arguments (با named routes)
Navigator.pushNamed(
context,
"/product",
arguments: product,
);
// در صفحه مقصد:
final product = ModalRoute.of(context)!.settings.arguments as Product;
دریافت نتیجه از صفحه
// منتظر نتیجه
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ConfirmPage()),
);
if (result == true) {
// کاربر تأیید کرد
}
// در ConfirmPage
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text("تأیید"),
)
Named Routes
MaterialApp(
initialRoute: "/",
routes: {
"/": (_) => const HomePage(),
"/login": (_) => const LoginPage(),
"/products": (_) => const ProductsPage(),
"/profile": (_) => const ProfilePage(),
},
// برای route های پیچیده
onGenerateRoute: (settings) {
if (settings.name == "/product") {
final id = settings.arguments as int;
return MaterialPageRoute(
builder: (_) => ProductDetailPage(productId: id),
);
}
return null;
},
)
Navigator.pushNamed(context, "/login");
۵.۳ go_router — راهحل مدرن
go_router توسط Flutter team توسعه داده شده و راه پیشنهادی برای ناوبری در پروژههای مدرن است.
flutter pub add go_router
import "package:go_router/go_router.dart";
// تعریف router
final router = GoRouter(
initialLocation: "/",
routes: [
GoRoute(
path: "/",
builder: (context, state) => const HomePage(),
),
GoRoute(
path: "/login",
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: "/products",
builder: (context, state) => const ProductsPage(),
routes: [
// nested
GoRoute(
path: ":id", // /products/123
builder: (context, state) {
final id = int.parse(state.pathParameters["id"]!);
return ProductDetailPage(productId: id);
},
),
],
),
],
// redirect برای auth
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn;
final isLoginPage = state.matchedLocation == "/login";
if (!isLoggedIn && !isLoginPage) return "/login";
if (isLoggedIn && isLoginPage) return "/";
return null;
},
// 404 handler
errorBuilder: (context, state) => Scaffold(
body: Center(child: Text("صفحه پیدا نشد: ${state.error}")),
),
);
// در MaterialApp.router
MaterialApp.router(
routerConfig: router,
title: "اپ من",
)
// ناوبری
context.go("/products"); // navigation
context.push("/products/123"); // push to stack
context.pop(); // back
context.replace("/login"); // replace
Query Parameters
GoRoute(
path: "/search",
builder: (context, state) {
final query = state.uri.queryParameters["q"] ?? "";
final category = state.uri.queryParameters["cat"];
return SearchPage(query: query, category: category);
},
)
// ناوبری با query
context.go("/search?q=laptop&cat=electronics");
۵.۴ Bottom Navigation با ShellRoute
final router = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) => MainShell(child: child),
routes: [
GoRoute(
path: "/",
builder: (_, __) => const HomeTab(),
),
GoRoute(
path: "/products",
builder: (_, __) => const ProductsTab(),
),
GoRoute(
path: "/cart",
builder: (_, __) => const CartTab(),
),
GoRoute(
path: "/profile",
builder: (_, __) => const ProfileTab(),
),
],
),
],
);
class MainShell extends StatelessWidget {
final Widget child;
const MainShell({super.key, required this.child});
int _calculateSelectedIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
if (location.startsWith("/products")) return 1;
if (location.startsWith("/cart")) return 2;
if (location.startsWith("/profile")) return 3;
return 0;
}
void _onTap(BuildContext context, int index) {
switch (index) {
case 0: context.go("/"); break;
case 1: context.go("/products"); break;
case 2: context.go("/cart"); break;
case 3: context.go("/profile"); break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _calculateSelectedIndex(context),
onTap: (i) => _onTap(context, i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "خانه"),
BottomNavigationBarItem(icon: Icon(Icons.shopping_bag), label: "محصولات"),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: "سبد"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "پروفایل"),
],
),
);
}
}
۵.۵ Dialogها و Bottom Sheetها
// AlertDialog
final result = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("حذف؟"),
content: const Text("آیا مطمئن هستید؟"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text("لغو"),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text("حذف"),
),
],
),
);
// SimpleDialog
showDialog(
context: context,
builder: (_) => SimpleDialog(
title: const Text("انتخاب زبان"),
children: [
SimpleDialogOption(
onPressed: () => Navigator.pop(context, "fa"),
child: const Text("فارسی"),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, "en"),
child: const Text("English"),
),
],
),
);
// Bottom Sheet
showModalBottomSheet(
context: context,
isScrollControlled: true, // برای ارتفاع بزرگ
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("گزینهها", style: TextStyle(fontSize: 20)),
ListTile(leading: const Icon(Icons.share), title: const Text("اشتراکگذاری")),
ListTile(leading: const Icon(Icons.delete), title: const Text("حذف")),
],
),
),
);
// SnackBar (پیام موقت)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text("ذخیره شد"),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: "بازگردانی",
onPressed: () {},
),
),
);
۵.۶ Deep Linking
Deep link به کاربر امکان میدهد از URL یا notification مستقیم به یک صفحه خاص بیاید.
Android — تنظیم intent-filter
<!-- android/app/src/main/AndroidManifest.xml -->
<activity ...>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="shop.icsd.ir"
android:pathPrefix="/products" />
</intent-filter>
</activity>
iOS — Universal Links
<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>icsdshop</string>
</array>
</dict>
</array>
با go_router خودکار است!
final router = GoRouter(
routes: [
GoRoute(path: "/products/:id", builder: ...),
],
);
// زمانی که کاربر روی https://shop.icsd.ir/products/123 کلیک کند،
// اپ باز میشود و مستقیم به ProductDetailPage(123) میرود
۵.۷ Hero Animation
انیمیشن زیبا بین دو صفحه — یک widget از یک صفحه به صفحه دیگر «پرواز» میکند.
// در صفحه اول
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.push("/product/${product.id}"),
child: Hero(
tag: "product-${product.id}", // ← باید unique باشد
child: Image.network(product.imageUrl),
),
);
}
}
// در صفحه دوم
class ProductDetailPage extends StatelessWidget {
final Product product;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Hero(
tag: "product-${product.id}", // ← همان tag
child: Image.network(product.imageUrl),
),
// ادامه ...
],
),
);
}
}
۵.۸ بهترین تجربیات
- go_router را انتخاب کنید. برای پروژههای جدید.
- Type-safe routes. با enum یا constants.
- Auth redirect. در router نه در widget ها.
- Deep links از روز اول.
- Hero animations. برای UX بهتر.
- WillPopScope برای handling back. برای فرمهای نیمهتکمیل.
- context.mounted check. بعد از await.
- Named routes یا paths constant. برای جلوگیری از typo.
۵.۹ خلاصه فصل
- Navigator 1.0: push، pop، pushReplacement
- go_router: declarative routing مدرن
- Pass data بین صفحات با constructor یا arguments
- ShellRoute برای bottom nav
- Dialog، BottomSheet، SnackBar
- Deep linking برای Android و iOS
- Hero animation برای UX زیبا
در فصل بعد: State Management — مهمترین مفهوم Flutter. Provider، Riverpod، Bloc.