DEV Community

Arslan Yousaf
Arslan Yousaf

Posted on

Migrating 10k Lines of Code from Navigation 1.0/GetX to GoRouter 2.0: A Case Study

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}
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

2. Planning the Migration

We decided on a phased approach:

  1. Setup GoRouter configuration
  2. Migrate public routes
  3. Add authentication handling
  4. Migrate protected routes
  5. Implement nested navigation
  6. 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
  ],
);
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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...
  ],
),
Enter fullscreen mode Exit fullscreen mode

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);
      },
    ),
  ],
);
Enter fullscreen mode Exit fullscreen mode

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),
);
Enter fullscreen mode Exit fullscreen mode

Solution:

// GoRouter solution
context.go('/alert/${alert.id}?type=${alert.type}');

// Parameter extraction
final alertId = state.params['id']!;
final alertType = state.queryParams['type'];
Enter fullscreen mode Exit fullscreen mode

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,
        );
      },
    );
  },
),
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  });
}
Enter fullscreen mode Exit fullscreen mode

Results and Lessons Learned

  1. Improved Code Organization

    • Centralized routing configuration
    • Better type safety
    • Easier to maintain
  2. Performance Improvements

    • Faster navigation
    • Reduced memory usage
    • Better deep linking support
  3. Development Experience

    • More predictable navigation
    • Easier debugging
    • Better IDE support
  4. Key Lessons

    • Plan the migration thoroughly
    • Test extensively
    • Document everything
    • Train the team on new patterns

Migration Tips

  1. Start Small

    • Begin with simple routes
    • Gradually move to complex ones
    • Keep old and new systems running parallel initially
  2. Test Everything

    • Unit tests for route logic
    • Widget tests for navigation
    • Integration tests for flows
  3. 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!

flutter #navigation #gorouter #migration #casestudy #mobile

Top comments (0)