NavigationUtils allows you to pause and defer navigation until asynchronous operations complete. This is essential for scenarios like authentication checks, data loading, or initialization that must complete before the app can navigate.
Why pause navigation?
When your app starts, you often need to:
- Check authentication status
- Load user preferences
- Initialize services
- Fetch configuration data
Without pausing navigation, the app might try to navigate before these operations complete, leading to incorrect routing or errors.
Basic usage
Pause navigation before runApp
Call pauseNavigation() before running your app:void main() {
WidgetsFlutterBinding.ensureInitialized();
NavigationManager.init(
mainRouterDelegate: DefaultRouterDelegate(
navigationDataRoutes: routes,
debugLog: true
),
routeInformationParser: DefaultRouteInformationParser(debugLog: true)
);
// Pause navigation until auth check completes
NavigationManager.instance.pauseNavigation();
runApp(const MyApp());
}
Perform async operations
Complete your initialization work:class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
// Mock authentication delay
Future.delayed(const Duration(milliseconds: 5000)).then((value) {
NavigationManager.instance.resumeNavigation();
});
}
}
Resume navigation
Call resumeNavigation() when ready:NavigationManager.instance.resumeNavigation();
Complete example
Here’s a full example from /home/daytona/workspace/source/example/lib/main_auth_delay.dart:1:
void main() {
WidgetsFlutterBinding.ensureInitialized();
if (kIsWeb) {
usePathUrlStrategy();
}
NavigationManager.init(
mainRouterDelegate:
DefaultRouterDelegate(navigationDataRoutes: routes, debugLog: true),
routeInformationParser: DefaultRouteInformationParser(debugLog: true)
);
NavigationManager.instance.pauseNavigation();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
// Mock Auth delay.
Future.delayed(const Duration(milliseconds: 5000)).then((value) {
NavigationManager.instance.resumeNavigation();
});
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Navigation Utils Demo',
routerDelegate: NavigationManager.instance.routerDelegate,
routeInformationParser: NavigationManager.instance.routeInformationParser,
);
}
}
Default loading screen
By default, pauseNavigation() displays a loading screen with a CupertinoActivityIndicator:
NavigationManager.instance.pauseNavigation();
This shows:
- A blank scaffold
- A centered loading indicator
- No navigation is possible until resumed
Custom loading screen
Provide your own loading page with the pageBuilder parameter:
NavigationManager.instance.pauseNavigation(
pageBuilder: (name) => MaterialPage(
name: name,
child: Scaffold(
backgroundColor: Colors.blue,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/logo.png'),
const SizedBox(height: 24),
const CircularProgressIndicator(color: Colors.white),
const SizedBox(height: 16),
const Text(
'Loading...',
style: TextStyle(color: Colors.white, fontSize: 18),
),
],
),
),
),
),
);
Real-world authentication example
void main() async {
WidgetsFlutterBinding.ensureInitialized();
NavigationManager.init(
mainRouterDelegate: DefaultRouterDelegate(
navigationDataRoutes: routes
),
routeInformationParser: DefaultRouteInformationParser()
);
// Pause until auth check completes
NavigationManager.instance.pauseNavigation(
pageBuilder: (name) => MaterialPage(
name: name,
child: const SplashScreen(),
),
);
runApp(const MyApp());
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_initializeApp();
}
Future<void> _initializeApp() async {
// Check authentication
bool isAuthenticated = await AuthService.instance.checkAuth();
// Load user preferences
await PreferencesService.instance.load();
// Initialize analytics
await AnalyticsService.instance.init();
// Navigate to appropriate initial screen
if (isAuthenticated) {
NavigationManager.instance.set(['/home']);
} else {
NavigationManager.instance.set(['/login']);
}
// Resume navigation
NavigationManager.instance.resumeNavigation();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'My App',
routerDelegate: NavigationManager.instance.routerDelegate,
routeInformationParser: NavigationManager.instance.routeInformationParser,
);
}
}
How it works
pauseNavigation() uses setOverride() under the hood, which preserves the URL and removes all existing pages. The loading screen is displayed instead of the normal navigation stack.
When you call pauseNavigation():
- The current navigation stack is overridden
- Only the loading page is displayed
- The URL is preserved (important for deep links)
- No navigation can occur
When you call resumeNavigation():
- The override is removed
- Normal navigation resumes
- The app navigates to the appropriate route
Best practices
Pause before runApp
Always call pauseNavigation() before runApp() to ensure the loading screen appears immediately.
Set navigation stack before resuming
Use NavigationManager.instance.set() to configure the initial navigation stack based on your app state before calling resumeNavigation().
Handle errors
Catch errors in your async operations to ensure resumeNavigation() is always called:try {
await AuthService.instance.checkAuth();
} catch (e) {
debugPrint('Auth error: $e');
NavigationManager.instance.set(['/error']);
} finally {
NavigationManager.instance.resumeNavigation();
}
Provide visual feedback
Use a custom loading screen that matches your app’s branding and provides appropriate feedback to users.
Common patterns
Progressive initialization
Future<void> _initializeApp() async {
// Quick local checks first
final hasToken = await SecureStorage.instance.hasToken();
if (!hasToken) {
NavigationManager.instance.set(['/login']);
NavigationManager.instance.resumeNavigation();
return;
}
// Slower network checks after
try {
final user = await AuthService.instance.validateToken();
NavigationManager.instance.set(['/home']);
} catch (e) {
NavigationManager.instance.set(['/login']);
}
NavigationManager.instance.resumeNavigation();
}
Timeout handling
Future<void> _initializeApp() async {
try {
await Future.any([
_loadUserData(),
Future.delayed(const Duration(seconds: 10)),
]);
} catch (e) {
debugPrint('Initialization timeout or error');
} finally {
NavigationManager.instance.resumeNavigation();
}
}