• Flutter
  • Web
  • Navigation
  • GoRouter
  • GetX

Flutter Web Navigation: GoRouter + GetX (No GetMaterialApp)

Aman Kumar

Aman Kumar

Flutter Developer, Gigawave

6 min read · June 14, 2025

Real-Life Dev Woes (Funny but True)

You start with GetX. Life seems good. You use Get.to() and Get.toNamed(). Suddenly:

  • Get.snackbar() : Ghosted. No show.
  • Get.dialog() : Vanishes faster than your motivation on Monday.
  • Get.bottomSheet() : Absolutely nothing. Dead.
  • Back Button : Nope. Stack who? History where?
Wait... Flutter Web is supposed to work, right? 🤔But overlays and navigation break in real life!

Problems With Get.to() & GetMaterialApp in Web

  • Overlays stop rendering : snackbar/dialog/sheet
  • Back button doesn't push or pop as expected
  • Navigation stack not preserved properly
  • Page refreshes behave unpredictably
  • Deep links break unless handled manually
  • GoRouter? GoneRouter.

Deep Links and Web Features Are Broken:

  • /route-name : direct open = broken or 404
  • Reload : new app instance
  • Back/forward : random behavior

GoRouter to the Rescue (Yes, Even With GetX)

Instead of using GetMaterialApp, use MaterialApp.router and let GoRouter do what it does best—ROUTING. And let GetX be the boss of overlays, snackbars, and state management.

  • Route Guards : Redirect unauthenticated users in one line
  • Shell Routes : Shared UI (e.g., bottom nav) across child pages
  • Nested Routes : Easily manage parent/child paths
  • Error Pages : Define beautiful 404s, not random crashes
  • Redirects : Funnel users based on auth/state easily

The Fix: MaterialApp.router + Get.key

app/main.dart
1void main() { 2 runApp(MyApp()); 3} 4 5class MyApp extends StatelessWidget { 6 final _router = GoRouter( 7 navigatorKey: Get.key, 8 routes: [ 9 GoRoute( 10 path: '/', 11 builder: (context, state) => HomePage(), 12 ), 13 GoRoute( 14 path: '/about', 15 builder: (context, state) => AboutPage(), 16 ), 17 ], 18 ); 19 20 21 Widget build(BuildContext context) { 22 return MaterialApp.router( 23 routerConfig: _router, 24 navigatorKey: Get.key, 25 ); 26 } 27}
CodeExplanation
navigatorKey: Get.key
Enables GetX to show overlays like snackbar, dialog, bottomsheet
MaterialApp.router
Required by GoRouter for structured web routing
GoRoute
Defines your routes in a declarative, clean way

End Result

  • Overlays work (snackbar, dialog, bottomSheet)
  • Deep links respected
  • Back/forward button works like normal websites
  • Route protection, nested routes, shell routes
  • Structured navigation logic

Things to NEVER DO

  • Don't use GetMaterialApp in Flutter Web
  • Don't manage routes manually via Get.to() in large apps
  • Don't ignore navigatorKey if you're mixing GetX + GoRouter
Pro tipLet GoRouter handle routes, let GetX handle overlays. Boom—clean, modern, working Flutter Web app.

Passing & Persisting Data withstate.extra in GoRouter

Need to pass arguments across routes, even on web reloads or back/forward presses? Here's how to use state.extra with fallback logic for a smooth experience.

1. Define Your Arguments

app/main.dart
1class ProductDetailArgs { 2 final ProductPreviewModel? proPreview; 3 final ProductModel? product; 4 5 ProductDetailArgs({this.proPreview, this.product}); 6}

2. Push With extra

app/main.dart
1GoRouter.of(context).pushNamed( 2 WebRouteNames.webProductDetailPage, 3 extra: ProductDetailArgs( 4 proPreview: preview, 5 product: product, 6 ), 7);

3. Receive & Persist Inside Route

Important: If you're using back/forward buttons or refreshing the browser, state.extra will be null. So you must persist the data manually.

ImportantPersist data manually if state.extra is null.
app/main.dart
1ProductDetailArgs? savedArg; 2 3GoRoute( 4 path: WebRouteNames.webProductDetailPage, 5 builder: (context, state) { 6 ProductDetailArgs? args = state.extra as ProductDetailArgs?; 7 if (args != null) { 8 savedArg = args; 9 } 10 11 return WebProductDetailsPage( 12 proPreview: savedArg?.proPreview, 13 product: savedArg?.product, 14 ); 15 }, 16);

Why Save to a Variable?

ReasonWhen/Why
state.extra is null
on browser refresh
Back button doesn't re-call
pushNamed()
Prevents crashes
and allows fallback

Bonus: Persist via Local Storage

For full persistence (e.g. on refresh), you can save to local/session storage:

app/main.dart
1// Save 2window.localStorage.setItem('last_product', jsonEncode(args)); 3 4// Retrieve (on null extra) 5var storedData = window.localStorage.getItem('last_product');

Example: OTP Page

app/main.dart
1GoRoute( 2 path: WebRouteNames.webOtpPage, 3 builder: (context, state) { 4 final args = state.extra as Map<String, dynamic>?; 5 final OTPType otpType = args?['otpType']; 6 final bool ifNewUser = args?['ifNewUser']; 7 8 return WebOTPPage( 9 otpType: otpType, 10 ifNewUser: ifNewUser, 11 ); 12 }, 13);

Handling Back Button With Arguments

When user presses back/forward in the browser, your page might rebuild without the arguments. Avoid crashes by:

  • Saving once from state.extra
  • Using fallback from saved variables
app/main.dart
1// Safe builder with fallback 2builder: (context, state) { 3 ProductDetailArgs? args = state.extra as ProductDetailArgs?; 4 if (args != null) savedArg = args; 5 6 return WebProductDetailsPage( 7 proPreview: savedArg?.proPreview, 8 product: savedArg?.product, 9 ); 10}

Summary

  • Pass arguments via extra
  • Save in local variables to persist state
  • Fallback on null for safe rebuilds
  • Optional: Use localStorage for cross-reloads

Pro tip: Use extra to keep your routing clean and argument-safe, especially on Flutter Web.

Pro tip:Use 'extra' for clean, argument-safe routing.
Next Blog

Flutter Fielding Position Picker Using CustomPainter

Read Next