Skip to main content
By default, hot reload doesn’t detect changes to your route definitions in Navigator 2. NavigationUtils provides a simple solution to enable hot reload for route changes.

The problem

When you add or modify routes during development, hot reload won’t apply the changes. This is because:
  • The Navigator widget is static to avoid rebuilding on every navigation event
  • Route lists are often defined as top-level variables
  • Flutter’s hot reload only reloads the widget build path
Without hot reload support, you’d need to perform a full restart every time you change routes, which is slow and interrupts your development flow.

The solution

Override the reassemble() method in your app widget to update routes on hot reload:
class _MyAppState extends State<MyApp> {
  @override
  void reassemble() {
    NavigationManager.instance.routerDelegate.navigationDataRoutes = routes;
    super.reassemble();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Navigation Utils Demo',
      routerDelegate: NavigationManager.instance.routerDelegate,
      routeInformationParser: NavigationManager.instance.routeInformationParser,
    );
  }
}

Complete example

Here’s a full working example from /home/daytona/workspace/source/example/lib/main.dart:37:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:navigation_utils/navigation_utils.dart';

List<NavigationData> routes = [
  NavigationData(
    label: MyHomePage.name,
    url: '/',
    builder: (context, routeData, globalData) => const MyHomePage()
  ),
  NavigationData(
    label: ProjectPage.name,
    url: '/project/:id',
    builder: (context, routeData, globalData) => ProjectPage(
      id: int.tryParse(routeData.pathParameters['id'] ?? '') ?? 0
    )
  ),
];

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  if (kIsWeb) {
    usePathUrlStrategy();
  }
  NavigationManager.init(
    mainRouterDelegate: DefaultRouterDelegate(navigationDataRoutes: routes),
    routeInformationParser: DefaultRouteInformationParser()
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  /// Enables hot reload for navigation route changes. Without this, adding new routes
  /// and any route changes won't apply with hot reload because the Navigator is static.
  @override
  void reassemble() {
    NavigationManager.instance.routerDelegate.navigationDataRoutes = routes;
    super.reassemble();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Navigation Utils Demo',
      routerDelegate: NavigationManager.instance.routerDelegate,
      routeInformationParser: NavigationManager.instance.routeInformationParser,
    );
  }
}

How it works

1

reassemble() is called on hot reload

When you hot reload your app, Flutter calls reassemble() on all widget states.
2

Routes are updated

The method updates the router delegate’s route list with the latest definitions:
NavigationManager.instance.routerDelegate.navigationDataRoutes = routes;
3

Navigator rebuilds with new routes

The Navigator now has access to the updated routes and can use them immediately.

Important requirements

For hot reload to work properly with routes, follow these requirements:

Use getters for routes

Define your routes using a getter instead of a final variable:
// ✅ Good - uses getter
List<NavigationData> get routes => [
  NavigationData(
    url: '/',
    builder: (context, routeData, globalData) => HomePage(),
  ),
];

// ❌ Bad - uses final variable
final List<NavigationData> routes = [
  NavigationData(
    url: '/',
    builder: (context, routeData, globalData) => HomePage(),
  ),
];
Why? Top-level final variables are not re-initialized on hot reload. Using a getter ensures the route list is regenerated each time it’s accessed.

Avoid final references

Don’t cache route references in final variables:
// ✅ Good
class _MyAppState extends State<MyApp> {
  @override
  void reassemble() {
    NavigationManager.instance.routerDelegate.navigationDataRoutes = routes;
    super.reassemble();
  }
}

// ❌ Bad
class _MyAppState extends State<MyApp> {
  final routeList = routes; // This won't update on hot reload
  
  @override
  void reassemble() {
    NavigationManager.instance.routerDelegate.navigationDataRoutes = routeList;
    super.reassemble();
  }
}

Update static Navigator variables

The Navigator is intentionally static to avoid rebuilding on every navigation event. The reassemble() method updates these internal static variables so they reflect your route changes.

What gets hot reloaded

With this setup, you can hot reload changes to:
  • Adding new routes
  • Removing routes
  • Changing route URLs
  • Modifying route builders
  • Updating route metadata
  • Changing page types
  • Modifying route parameters

Example workflow

1

Start with initial routes

List<NavigationData> get routes => [
  NavigationData(
    url: '/',
    builder: (context, routeData, globalData) => HomePage(),
  ),
];
2

Add a new route

List<NavigationData> get routes => [
  NavigationData(
    url: '/',
    builder: (context, routeData, globalData) => HomePage(),
  ),
  NavigationData(
    url: '/settings',
    builder: (context, routeData, globalData) => SettingsPage(),
  ),
];
3

Hot reload

Press r in your terminal or click the hot reload button. The new route is immediately available without a full restart.
4

Navigate to new route

NavigationManager.instance.push('/settings');
The new route works immediately!

Why reassemble() works

The reassemble() method is part of Flutter’s hot reload mechanism:
  • Called on every hot reload
  • Runs before the build method
  • Perfect for updating static or cached data
  • Doesn’t affect production builds (only runs during development)

Production impact

The reassemble() method only runs during development hot reload. It has zero impact on production builds or app performance.

Troubleshooting

Routes not updating on hot reload

Problem: You press hot reload but your route changes don’t appear. Solution: Check that:
  1. You’re using a getter for routes, not a final variable
  2. You’ve added the reassemble() method to your app widget
  3. You’re calling super.reassemble() after updating routes

Need full restart for route changes

Problem: You still need to fully restart the app for route changes. Solution: This usually means you’re using final variables somewhere. Convert to getters:
// Change this:
final routes = [...];

// To this:
List<NavigationData> get routes => [...];

Hot reload works but navigation fails

Problem: Hot reload updates routes, but navigating to new routes fails. Solution: Make sure your route definitions are correct:
  • URLs start with /
  • Builders return valid widgets
  • Labels don’t conflict with existing routes

Alternative: Use route files

For larger apps, consider organizing routes in separate files:
// routes/app_routes.dart
List<NavigationData> get appRoutes => [
  ...homeRoutes,
  ...profileRoutes,
  ...settingsRoutes,
];

// routes/home_routes.dart
List<NavigationData> get homeRoutes => [
  NavigationData(
    url: '/',
    builder: (context, routeData, globalData) => HomePage(),
  ),
];
Then use in your app:
class _MyAppState extends State<MyApp> {
  @override
  void reassemble() {
    NavigationManager.instance.routerDelegate.navigationDataRoutes = appRoutes;
    super.reassemble();
  }
}
This keeps your code organized while maintaining hot reload functionality.

Build docs developers (and LLMs) love