~/icsd.ir — bash
SYSTEM_ONLINE

ناوبری و Routing

ناوبری بین صفحات یکی از مهم‌ترین قسمت‌های هر اپلیکیشن است. Flutter دو سیستم ناوبری دارد: Navigator 1.0 (ساده) و Navigator 2.0 (declarative). همچنین package های مدرنی مثل go_router کار را راحت‌تر می‌کنند.

۵.۱ مقدمه

ناوبری بین صفحات یکی از مهم‌ترین قسمت‌های هر اپلیکیشن است. Flutter دو سیستم ناوبری دارد: Navigator 1.0 (ساده) و Navigator 2.0 (declarative). همچنین package های مدرنی مثل go_router کار را راحت‌تر می‌کنند.


// رفتن به صفحه جدید
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 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),
          ),
          // ادامه ...
        ],
      ),
    );
  }
}
    

۵.۸ بهترین تجربیات

  1. go_router را انتخاب کنید. برای پروژه‌های جدید.
  2. Type-safe routes. با enum یا constants.
  3. Auth redirect. در router نه در widget ها.
  4. Deep links از روز اول.
  5. Hero animations. برای UX بهتر.
  6. WillPopScope برای handling back. برای فرم‌های نیمه‌تکمیل.
  7. context.mounted check. بعد از await.
  8. 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.

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.