本文へ移動
cccskills
無料GitHub で公開

routing-with-gorouter

Implements declarative Flutter routing using GoRouter v17 with URL-based navigation, deep linking, and authentication guards. Activates when configuring GoRoute path patterns, setting up StatefulShellRoute for persistent bottom navigation state, implementing ShellRoute for shared nested layouts, adding redirect guards with loop prevention (idempotent redirects), configuring deep links for iOS (FlutterDeepLinkingEnabled) or Android (intent-filters), debugging redirect loops or navigation stack issues, handling 404 error routes with errorBuilder, using named routes with path/query/extra parameters, or migrating from imperative Navigator.push to declarative routing. Ideal for Flutter web apps requiring URL bar synchronization, mobile apps with universal links or app links, or multi-level navigation hierarchies.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md13.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

GoRouter Declarative Navigation Guide (v17.x)

Goal

Implement declarative routing using GoRouter, the official Flutter navigation solution. GoRouter provides URL-based navigation, deep linking support, and declarative route configuration ideal for web and mobile apps.

Process

Phase 1: Install Dependencies

dependencies:
  go_router: ^17.2.0

dev_dependencies:
  go_router_builder: ^4.3.0  # Optional: for type-safe routes

Phase 2: Define Routes

import 'package:go_router/go_router.dart';

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => const HomeScreen(),
      routes: [
        // Nested route: /profile
        GoRoute(
          path: 'profile',
          name: 'profile',
          builder: (context, state) => const ProfileScreen(),
        ),
        // Parameterized route: /user/:id
        GoRoute(
          path: 'user/:id',
          name: 'user',
          builder: (context, state) {
            final userId = state.pathParameters['id']!;
            return UserScreen(userId: userId);
          },
        ),
      ],
    ),
    GoRoute(
      path: '/login',
      name: 'login',
      builder: (context, state) => const LoginScreen(),
    ),
  ],
);

Phase 3: Use Router in App

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

Phase 4: Navigation

Imperative Navigation:

// Push by path
context.go('/profile');

// Push by name
context.goNamed('user', pathParameters: {'id': '123'});

// Push by name with query params
context.goNamed('search', queryParameters: {'q': 'flutter'});

// Pop
context.pop();

// Replace
context.pushReplacement('/login');

Declarative Navigation (recommended):

// Instead of buttons with onPressed
TextButton(
  onPressed: () => context.go('/profile'),
  child: Text('Go to Profile'),
)

Phase 5: Persistent Bottom Navigation with StatefulShellRoute

final router = GoRouter(
  initialLocation: '/home',
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithNavBar(navigationShell: navigationShell);
      },
      branches: [
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/home',
              builder: (context, state) => const HomeScreen(),
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/search',
              builder: (context, state) => const SearchScreen(),
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/profile',
              builder: (context, state) => const ProfileScreen(),
            ),
          ],
        ),
      ],
    ),
  ],
);

class ScaffoldWithNavBar extends StatelessWidget {
  final StatefulNavigationShell navigationShell;
  
  const ScaffoldWithNavBar({required this.navigationShell});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: navigationShell.currentIndex,
        onTap: (index) => navigationShell.goBranch(index),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }
}

Phase 6: Authentication Guards & Redirects

CRITICAL: Prevent Redirect Loops

Redirect callbacks MUST be idempotent — returning null when no redirect is needed. Infinite loops occur when redirect logic doesn't check the current location before redirecting.

❌ BAD - Causes Infinite Loop:

redirect: (context, state) {
  final isLoggedIn = AuthService.instance.isLoggedIn;
  
  // DANGER: Always redirects when not logged in, even if already at /login
  if (!isLoggedIn) {
    return '/login'; // Loop: /login → /login → /login...
  }
  
  return null;
}

✅ GOOD - Idempotent Redirect:

final router = GoRouter(
  redirect: (context, state) {
    final isLoggedIn = AuthService.instance.isLoggedIn;
    final currentPath = state.uri.toString(); // Use uri.toString() not matchedLocation
    
    // Only redirect IF needed (check current path first)
    if (!isLoggedIn && currentPath != '/login') {
      return '/login';
    }
    
    // Redirect to home if already logged in and trying to access login
    if (isLoggedIn && currentPath == '/login') {
      return '/';
    }
    
    // ALWAYS return null when no redirect needed
    return null;
  },
  refreshListenable: AuthService.instance, // Re-evaluate on auth change
  routes: [...],
);

class AuthService extends ChangeNotifier {
  static final instance = AuthService();
  
  bool _isLoggedIn = false;
  bool get isLoggedIn => _isLoggedIn;
  
  void login() {
    _isLoggedIn = true;
    notifyListeners(); // Triggers redirect re-evaluation
  }
  
  void logout() {
    _isLoggedIn = false;
    notifyListeners();
  }
}

Redirect Best Practices:

  • Always check state.uri.toString() before redirecting
  • Return null when no redirect is needed (not the current path)
  • Use state.uri.toString() instead of state.matchedLocation for full URL including query params
  • Test redirect logic by navigating to protected routes while logged out

Phase 7: Deep Linking Configuration

Deep links allow external URLs to open specific screens in your app. GoRouter handles deep link routing automatically once platform configuration is complete.

iOS Deep Link Setup

1. Enable Deep Linking (ios/Runner/Info.plist):

<!-- REQUIRED for GoRouter deep linking to work -->
<key>FlutterDeepLinkingEnabled</key>
<true/>

<!-- Custom URL scheme (myapp://...) -->
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

2. Universal Links (Optional - for https://yourdomain.com links):

<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:yourdomain.com</string>
</array>

Also requires .well-known/apple-app-site-association file on your server.

Android Deep Link Setup

1. Intent Filters (android/app/src/main/AndroidManifest.xml):

<activity android:name=".MainActivity">
    <!-- Existing config... -->
    
    <!-- Custom URL scheme -->
    <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="myapp" android:host="example.com" />
    </intent-filter>
    
    <!-- HTTPS deep links (App Links) -->
    <intent-filter android:autoVerify="true">
        <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="yourdomain.com" />
    </intent-filter>
</activity>

2. App Links Verification (Optional): Requires .well-known/assetlinks.json on your server.

Deep Links to Nested Routes

Problem: Deep link myapp://app/parent/child opens at /parent instead of /parent/child.

Solution: Ensure route path structure matches URL structure exactly:

GoRoute(
  path: '/parent',
  builder: (context, state) => ParentPage(),
  routes: [
    // Nested path matches /parent/child
    GoRoute(
      path: 'child', // No leading slash - relative to parent
      builder: (context, state) => ChildPage(),
    ),
    // Parameterized nested route: /parent/child/:id
    GoRoute(
      path: 'child/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return ChildDetailPage(id: id);
      },
    ),
  ],
),

Test Deep Links:

# iOS Simulator
xcrun simctl openurl booted "myapp://app/parent/child"

# Android
adb shell am start -W -a android.intent.action.VIEW \
  -d "myapp://app/parent/child" com.example.app

Deep Links with StatefulShellRoute

When using StatefulShellRoute for bottom navigation, deep links work but may reset the navigation stack within that branch:

StatefulShellRoute.indexedStack(
  builder: (context, state, navigationShell) {
    return ScaffoldWithNavBar(navigationShell: navigationShell);
  },
  branches: [
    StatefulShellBranch(
      routes: [
        GoRoute(
          path: '/home',
          builder: (context, state) => const HomeScreen(),
          routes: [
            // Deep link: myapp://app/home/details/123
            GoRoute(
              path: 'details/:id',
              builder: (context, state) {
                final id = state.pathParameters['id']!;
                return DetailsScreen(id: id);
              },
            ),
          ],
        ),
      ],
    ),
  ],
),

Note: The state within the home branch is preserved during normal navigation, but deep linking directly to /home/details/123 creates a fresh navigation stack.

Phase 8: Debugging Common Issues

Redirect Loop Debugging

Symptoms:

  • App freezes on navigation
  • Console shows repeated redirect messages
  • Stack overflow errors

Solution: Add logging to redirect callback to trace the loop:

redirect: (context, state) {
  final isLoggedIn = AuthService.instance.isLoggedIn;
  final currentPath = state.uri.toString();
  
  print('🔀 Redirect check: $currentPath (logged in: $isLoggedIn)');
  
  if (!isLoggedIn && currentPath != '/login') {
    print('➡️ Redirecting to /login');
    return '/login';
  }
  
  if (isLoggedIn && currentPath == '/login') {
    print('➡️ Redirecting to /');
    return '/';
  }
  
  print('✅ No redirect needed');
  return null;
}

Look for repeated identical log lines — that's your loop.

Deep Link Not Working

Checklist:

  • iOS: FlutterDeepLinkingEnabled set to true in Info.plist
  • Android: Intent filter includes all three required elements (action, category, data)
  • Route path exactly matches deep link path structure
  • Nested routes use relative paths (no leading slash)
  • Test with simulator/emulator commands, not just tapping links
  • Check adb logcat (Android) or Xcode console (iOS) for errors

404 Error Routes

Handle unknown routes gracefully:

final router = GoRouter(
  routes: [...],
  errorBuilder: (context, state) => ErrorScreen(
    error: state.error,
    path: state.uri.toString(),
  ),
);

class ErrorScreen extends StatelessWidget {
  final Object? error;
  final String path;
  
  const ErrorScreen({required this.error, required this.path});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Page Not Found')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, size: 48),
            const SizedBox(height: 16),
            Text('The page "$path" could not be found.'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => context.go('/'),
              child: const Text('Go Home'),
            ),
          ],
        ),
      ),
    );
  }
}

Constraints

  • Declarative Only: Avoid imperative Navigator.push. Use context.go/goNamed.
  • Path Parameters: Always validate path parameters before use.
  • Web Compatibility: All routes must maintain URL bar synchronization.
  • Redirect Loops: ALWAYS check current path before redirecting; return null when no redirect needed.
  • Deep Link Testing: Test on real devices/simulators, not just browsers.
  • State Preservation: Use StatefulShellRoute for tabs to preserve state.
  • Nested Routes: Use relative paths (no leading /) for child routes.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Declarative animation patterns using flutter_animate v4.5.x with composable effects (fade, scale, slide, shimmer, shake, blur), GLSL fragment shaders, and scroll-synchronized animations. Use this skill for adding UI animations and transitions, building staggered or sequenced effects, creating custom animation effects, fixing animation jank or performance issues, implementing shimmer loading states, or adding scroll-driven animations. Supports delays, curves, adaptive effects, and easy extension method chaining (.animate()).

日本語の概要は準備中です。原文の説明を表示しています。

Poorgramer-Zack/dart-expert-skills72026年8月11日 更新

Dart 3 modern features (through 3.10) and Effective Dart best practices including records, patterns, sealed classes, dot shorthands, null-aware collection elements, and wildcard variables. Use when adopting Dart 3.7+ syntax, using dot shorthands for enums or static members, using null-aware elements in lists or maps, using wildcard _ parameters, improving code quality, or reviewing Dart code conventions.

日本語の概要は準備中です。原文の説明を表示しています。

Poorgramer-Zack/dart-expert-skills72026年8月11日 更新

Expert-level guidance for Flutter production applications: architecture decisions (Clean/Layered/Hexagonal/Feature-First), advanced state management (BLoC/Riverpod/GetX), performance optimization (eliminating jank, memory profiling, reducing rebuild overhead), code review with design pattern enforcement, scalability assessments, native platform integration (MethodChannel/EventChannel/FFI), complex UI challenges (custom render objects, slivers), testing strategies (unit/widget/integration/golden), CI/CD pipeline architecture, build optimization, and accessibility compliance. Triggers: 'architecture decision', 'design pattern', 'performance issue', 'code review', 'production problem', 'scaling concerns', 'best practices', 'technical debt', 'refactoring strategy'. Essential for complex enterprise apps, critical production debugging, architectural refactors, or when choosing between competing technical approaches.

日本語の概要は準備中です。原文の説明を表示しています。

Poorgramer-Zack/dart-expert-skills72026年8月11日 更新

Builds SEO-friendly web applications with Dart using the Jaspr framework. Use when creating Dart-based websites, blogs, or landing pages requiring semantic HTML output; implementing server-side rendering (SSR) or static site generation (SSG); adding Tailwind CSS via jaspr_tailwind; routing with jaspr_router; testing components with jaspr_test; or building full-stack Dart web apps where Flutter Web's canvas rendering is unsuitable.

日本語の概要は準備中です。原文の説明を表示しています。

Poorgramer-Zack/dart-expert-skills72026年8月11日 更新

shadcn_flutter UI component library for building accessible, themed Flutter interfaces. Use when integrating shadcn components, configuring design system theming, or building consistent widget layouts.

日本語の概要は準備中です。原文の説明を表示しています。

Poorgramer-Zack/dart-expert-skills72026年8月11日 更新

Configures Codemagic CI/CD pipelines for Flutter using `codemagic.yaml`. Use when setting up automated Flutter builds on Codemagic, configuring iOS code signing without Fastlane Match, deploying to App Store Connect or Google Play from Codemagic, caching pub dependencies in Codemagic, setting up encrypted environment variable groups, running tests with Codemagic's test dashboard, or choosing between Codemagic and GitHub Actions + Fastlane for a Flutter CI/CD pipeline.

日本語の概要は準備中です。原文の説明を表示しています。

Poorgramer-Zack/dart-expert-skills72026年8月11日 更新

Poorgramer-Zack のスキルをすべて見る

このスキルの問題を報告する