routing-with-gorouter — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited routing-with-gorouter (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
dependencies:
go_router: ^17.2.0
dev_dependencies:
go_router_builder: ^4.3.0 # Optional: for type-safe routesimport '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(),
),
],
);void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}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'),
)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'),
],
),
);
}
}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:
state.uri.toString() before redirectingnull when no redirect is needed (not the current path)state.uri.toString() instead of state.matchedLocation for full URL including query paramsDeep 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.
#### Redirect Loop Debugging
Symptoms:
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:
FlutterDeepLinkingEnabled set to true in Info.plistadb 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'),
),
],
),
),
);
}
}null when no redirect needed./) for child routes.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.