Introduction
Recently, I led the migration of UpAlerts, a large-scale Flutter application, from a mixed routing system using Navigation 1.0 and GetX to GoRouter 2.0. This post details our journey, challenges, and solutions, hoping to help others undertaking similar migrations.
Initial State
Our codebase had:
- ~10,000 lines of code
- Mixed routing approaches:
- Navigation 1.0 for basic navigation
- GetX for complex routes
- Custom route handlers for deep links
- Complex navigation patterns:
- Nested navigation in tabs
- Dynamic routes based on user roles
- Deep linking support
- Authentication-dependent routes
Original Route Structure
// Old routing setup with GetX
class Routes {
static final routes = [
GetPage(
name: '/dashboard',
page: () => DashboardScreen(),
middlewares: [AuthMiddleware()],
),
GetPage(
name: '/alerts/:id',
page: () => AlertDetailScreen(),
parameters: {'id': ''},
),
// More routes...
];
}
// Mixed with Navigation 1.0
Navigator.pushNamed(
context,
'/settings',
arguments: {'userId': currentUser.id}
);
Migration Strategy
1. Route Audit
First, we created a comprehensive route inventory:
1. Public Routes
- Login
- Register
- Forgot Password
- Landing Page
2. Protected Routes
- Dashboard
- Alert List
- Alert Details
- User Profile
- Settings
3. Nested Routes
- Alert Categories
- Category List
- Category Detail
- Add/Edit Category
4. Modal Routes
- Quick Actions
- Notifications
- Filters
2. Planning the Migration
We decided on a phased approach:
- Setup GoRouter configuration
- Migrate public routes
- Add authentication handling
- Migrate protected routes
- Implement nested navigation
- Add deep linking support
Implementation
Phase 1: GoRouter Setup
final GoRouter _router = GoRouter(
initialLocation: '/',
redirect: (context, state) async {
// Global redirect logic
final bool isLoggedIn = await AuthService().isLoggedIn();
final bool isGoingToLogin = state.location == '/login';
if (!isLoggedIn && !isGoingToLogin) {
return '/login';
}
if (isLoggedIn && isGoingToLogin) {
return '/dashboard';
}
return null;
},
routes: [
// Routes will be added here
],
);
Phase 2: Authentication Flow
One of our biggest challenges was handling authentication. We created a custom navigation guard:
class AuthGuard extends GoRouteData {
const AuthGuard();
@override
FutureOr<String?> redirect(BuildContext context, GoRouterState state) async {
final authService = Provider.of<AuthService>(context, listen: false);
final bool isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
return '/login?redirect=${state.location}';
}
return null;
}
}
Phase 3: Nested Navigation
Handling nested navigation was tricky. We used ShellRoute:
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(
path: '/alerts',
builder: (context, state) => const AlertsScreen(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) {
final alertId = state.params['id']!;
return AlertDetailScreen(alertId: alertId);
},
),
],
),
// More nested routes...
],
),
Phase 4: Deep Linking
Implementing deep linking with GoRouter was more straightforward than our previous solution:
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/alerts/:id/share',
builder: (context, state) {
final alertId = state.params['id']!;
return SharedAlertScreen(alertId: alertId);
},
),
],
);
Challenges and Solutions
1. Route Parameters Migration
Challenge: GetX and Navigation 1.0 handled route parameters differently than GoRouter.
Old Code:
// Using GetX
Get.toNamed('/alert/${alert.id}', arguments: {'type': alert.type});
// Using Navigation 1.0
Navigator.pushNamed(
context,
'/alert',
arguments: AlertScreenArgs(id: alert.id, type: alert.type),
);
Solution:
// GoRouter solution
context.go('/alert/${alert.id}?type=${alert.type}');
// Parameter extraction
final alertId = state.params['id']!;
final alertType = state.queryParams['type'];
2. Modal Routes
Challenge: Modal routes were previously handled with GetX's Get.dialog().
Solution:
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/filter',
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: const FilterScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: animation.drive(
Tween(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeInOut)),
),
child: child,
);
},
);
},
),
3. State Preservation
Challenge: Preserving screen state during navigation.
Solution:
class AlertListScreen extends StatefulWidget {
const AlertListScreen({super.key});
@override
State<AlertListScreen> createState() => _AlertListScreenState();
}
class _AlertListScreenState extends State<AlertListScreen>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
// Rest of the implementation
}
Testing Strategy
We implemented comprehensive testing for our routes:
void main() {
testWidgets('Authentication flow test', (tester) async {
final goRouter = GoRouter(
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/dashboard',
builder: (context, state) => const DashboardScreen(),
),
],
redirect: (context, state) async {
// Redirect logic
},
);
// Test implementation
});
}
Results and Lessons Learned
-
Improved Code Organization
- Centralized routing configuration
- Better type safety
- Easier to maintain
-
Performance Improvements
- Faster navigation
- Reduced memory usage
- Better deep linking support
-
Development Experience
- More predictable navigation
- Easier debugging
- Better IDE support
-
Key Lessons
- Plan the migration thoroughly
- Test extensively
- Document everything
- Train the team on new patterns
Migration Tips
-
Start Small
- Begin with simple routes
- Gradually move to complex ones
- Keep old and new systems running parallel initially
-
Test Everything
- Unit tests for route logic
- Widget tests for navigation
- Integration tests for flows
-
Document Changes
- Keep a migration guide
- Update team documentation
- Create example patterns
Conclusion
Migrating to GoRouter 2.0 was a significant undertaking but brought numerous benefits to our codebase. The improved type safety, better organization, and more maintainable code made the effort worthwhile.
Follow me for more Flutter development stories and tutorials!
Top comments (0)