advanced_nav_service 0.5.0 copy "advanced_nav_service: ^0.5.0" to clipboard
advanced_nav_service: ^0.5.0 copied to clipboard

A comprehensive navigation service package for Flutter applications providing routing, navigation state management, persistence, and navigation utilities.

Advanced Nav Service #

A powerful navigation service package for Flutter applications that provides advanced routing, navigation state management, and declarative navigation utilities.

Note: An alternative interface of this project is available at https://pub.dev/packages/flutter_nav

Table of Contents #

  1. Installation
  2. Features
  3. Standalone Setup
  4. Core Navigation
  5. Deep Linking
  6. Navigation Persistence
  7. GoRouter Integration
  8. Working with Extra Data
  9. Navigation History & Debugging
  10. API Reference
  11. Ultilities

1. Installation #

Add this package to your pubspec.yaml:

dependencies:
  advanced_nav_service: ^0.5.0

Then run:

flutter pub get

2. Features #

  • 🎯 Singleton Navigation Service: Access navigation functionality from anywhere in your app
  • 📊 Navigation History Tracking: Keep track of navigation stack and history
  • 💾 Extra Data Support: Pass and receive data between routes with type safety
  • 🔄 Advanced Route Management: Smart navigation, replace operations, and stack manipulation
  • 📝 Route Observers: Monitor navigation events with built-in observer
  • 🚀 Declarative API: Intuitive methods for all navigation scenarios
  • 🔍 Navigation Debugging: Built-in logging and navigation history inspection
  • ⚡ Performance Optimized: Efficient route management with minimal overhead
  • 🔗 Deep Linking Handling: Complete infrastructure for handling custom URLs with app_links integration, path parameters extraction, and flexible link handlers
  • 💾 Navigation Persistence: Automatic state persistence and restoration with customizable schedules
  • 🧰 Utilities: Other navigation utilities, make navigation easier and more efficient

3. Standalone Setup #

1. Define Your Routes #

import 'package:advanced_nav_service/nav_service.dart';

final routes = [
  NavRoute(
    path: '/home',
    builder: (context, state) => HomeScreen(state: state),
  ),
  NavRoute(
    path: '/profile',
    builder: (context, state) => ProfileScreen(state: state),
  ),
  NavRoute(
    path: '/settings',
    builder: (context, state) => SettingsScreen(state: state),
  ),
];

2. Initialize NavService #

void main() {
  final navigatorKey = GlobalKey<NavigatorState>();
  
  NavService.instance.init(
    NavServiceConfig(
      routes: routes,
      navigatorKey: navigatorKey,
      enableLogger: true,
    ),
  );
  
  runApp(MyApp(navigatorKey: navigatorKey));
}

3. Setup Your App #

class MyApp extends StatelessWidget {
  final GlobalKey<NavigatorState> navigatorKey;
  
  const MyApp({super.key, required this.navigatorKey});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      navigatorObservers: [NavService.instance.routeObserver],
      home: const SplashScreen(),
    );
  }
}

4. Handle App Launch #

Move initialization logic outside of widgets and use the launched() method to set initial routes:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final navigatorKey = GlobalKey<NavigatorState>();
  
  NavService.instance.init(
    NavServiceConfig(
      routes: routes,
      navigatorKey: navigatorKey,
      enableLogger: true,
    ),
  );
  
  runApp(MyApp(navigatorKey: navigatorKey));
  
  // Handle initial logic after app starts
  Future.delayed(const Duration(seconds: 2), () async {
    // Check authentication, deep links, etc.
    final bool isAuthenticated = await checkAuth();
    
    if (isAuthenticated) {
      NavService.instance.launched([NavRouteInfo(path: '/home')]);
    } else {
      NavService.instance.launched([NavRouteInfo(path: '/login')]);
    }
  });
}

class SplashScreen extends StatelessWidget {
  const SplashScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: CircularProgressIndicator()),
    );
  }
}

4. Core Navigation #

Basic Navigation #

// Push a new route
NavService.instance.push('/profile');

// Push with extra data
NavService.instance.push('/profile', extra: {
  'userId': 123,
  'name': 'John Doe',
});

// Pop current route
NavService.instance.pop();

// Pop with result data
NavService.instance.pop({'result': 'success'});

// Check if can pop
if (NavService.instance.canPop()) {
  NavService.instance.pop();
}

// Try to pop if possible and get whether pop occurred
if (NavService.instance.maybePop()) {
  // pop was performed
} else {
  // nothing to pop
}

Smart Navigation #

// Navigate intelligently - if route exists in stack, pop to it; otherwise push
NavService.instance.navigate('/home');

// Force push even if route exists in history
NavService.instance.navigate('/home', forcePush: true);

Replace Operations #

// Replace current route with push animation
NavService.instance.pushReplacement('/settings');

// Replace current route without animation
NavService.instance.replace('/settings');

Stack Management #

// Push and remove all previous routes
NavService.instance.pushAndRemoveUntil('/home', (route) => false);

// Pop until specific condition
NavService.instance.popUntil((route) => route.settings.name == '/home');

// Pop until specific path
NavService.instance.popUntilPath('/home');

// Pop all routes
NavService.instance.popAll();

// Remove all routes without animation
// Caution: just use this method when switch to gorouter
NavService.instance.removeAll();

Bulk Operations #

// Push multiple routes at once
NavService.instance.pushAll([
  NavRouteInfo(path: '/home'),
  NavRouteInfo(path: '/profile', extra: {'userId': 123}),
  NavRouteInfo(path: '/settings'),
]);

// Replace all routes with new stack
NavService.instance.replaceAll([
  NavRouteInfo(path: '/home'),
  NavRouteInfo(path: '/dashboard'),
]);

// Replace last route with multiple routes
NavService.instance.pushReplacementAll([
  NavRouteInfo(path: '/profile'),
  NavRouteInfo(path: '/edit'),
]);

5. Deep Linking #

Create custom link handlers by extending NavLinkHandler:

import 'package:advanced_nav_service/nav_service.dart';

class ProfileLinkHandler extends NavLinkHandler {
  @override
  List<String> get redirectPaths => [
    '/profile',
    '/profile/:id',
    '/user/:userId',
  ];

  @override
  void onRedirect(BuildContext context, NavLinkResult result) {
    // Handle the deep link navigation
    NavService.instance.navigate('/profile', extra: {
      ...result.pathParameters,  // e.g., {'id': '123'}
      ...result.queryParameters, // e.g., {'tab': 'settings'}
    });
  }
}

class SettingsLinkHandler extends NavLinkHandler {
  @override
  List<String> get redirectPaths => [
    '/settings',
    '/settings/:tab',
  ];

  @override
  void onRedirect(BuildContext context, NavLinkResult result) {
    NavService.instance.navigate('/settings', extra: {
      ...result.pathParameters,
      ...result.queryParameters,
    });
  }
}
  1. Install dependencies:
dependencies:
  advanced_nav_service: ^0.5.0
  app_links: ^latest_version
  1. Configure NavService with deep linking:
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final navigatorKey = GlobalKey<NavigatorState>();

  NavService.instance.init(
    NavServiceConfig(
      navigatorKey: navigatorKey,
      routes: routes,
      enableLogger: true,
      // Deep linking configuration
      linkPrefixes: [
        'myapp://',                    // Custom scheme
        'https://myapp.com/',          // Universal links
        'https://www.myapp.com/',      // Alternative domain
      ],
      linkHandlers: [
        ProfileLinkHandler(),
        SettingsLinkHandler(),
      ],
    ),
  );

  // Start the app first so the navigator and NavService are available.
  runApp(MyApp(navigatorKey: navigatorKey));

  // Initialize app_links integration after the first frame.
  // This ensures `NavService.instance.openUrl(...)` runs only when the
  // navigator and route observers are ready.
  WidgetsBinding.instance.addPostFrameCallback((_) {
    _initializeAppLinks();
  });
}

Future<void> _initializeAppLinks() async {
  final appLinks = AppLinks();

  // Handle initial link when app is launched
  final initialLink = await appLinks.getInitialLink();
  if (initialLink != null) {
    // Safe to open URL now that the app has been started
    NavService.instance.openUrl(initialLink.toString());
  }

  // Handle incoming links when app is running
  appLinks.uriLinkStream.listen((Uri uri) {
    NavService.instance.openUrl(uri.toString());
  });
}

// NOTE: If you handle initial logic in main() (see "Handle App Launch" above),
// coordinate the initial link handling with your launch logic to avoid
// duplicate navigation.

Usage #

// Open URLs programmatically
NavService.instance.openUrl('myapp://profile/123?tab=settings');
NavService.instance.openUrl('https://myapp.com/profile/456?source=share');

URL Pattern Features #

  • Static paths: /profile, /settings
  • Dynamic parameters: /user/:userId, /product/:id
  • Query parameters: Automatically parsed and available
  • Custom schemes: myapp://, yourapp://
  • Universal links: https://domain.com/

6. Navigation Persistence #

NavService provides built-in support for persisting and restoring navigation state across app restarts.

Setup Persistence #

First, add a storage dependency (example uses SharedPreferences):

dependencies:
  advanced_nav_service: ^0.5.0
  shared_preferences: ^latest_version

Configure persistence with SharedPreferences or any storage mechanism:

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:advanced_nav_service/nav_service.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final navigatorKey = GlobalKey<NavigatorState>();
  const restorationId = 'app_restoration_id';
  
  NavService.instance.init(
    NavServiceConfig(
      navigatorKey: navigatorKey,
      routes: routes,
      enableLogger: true,
      persistence: NavPagePersistence(
        onPersist: (routes) async {
          final pref = await SharedPreferences.getInstance();
          await pref.setString(restorationId, jsonEncode(routes));
        },
        onRestore: () async {
          final pref = await SharedPreferences.getInstance();
          final jsonString = pref.getString(restorationId);
          if (jsonString != null) {
            final List<dynamic> data = jsonDecode(jsonString);
            return List<Map<String, dynamic>>.from(data);
          }
          return [];
        },
        enableSchedule: true,
        schedule: const NavPagePersistenceSchedule(immediate: true),
      ),
    ),
  );
  
  runApp(MyApp(navigatorKey: navigatorKey));
  
  // Launch app with restoration or default routes
  Future.delayed(const Duration(seconds: 2), () {
    NavService.instance.launched([NavRouteInfo(path: '/home')]);
  });
}

Persistence Configuration #

NavPagePersistence properties:

  • onPersist - Callback to save navigation state (receives List<Map<String, dynamic>>)
  • onRestore - Callback to load navigation state (returns List<Map<String, dynamic>>)
  • enableSchedule - Enable automatic persistence on navigation events
  • schedule - Configure when to persist (immediate or interval-based)

NavPagePersistenceSchedule options:

  • immediate: true - Save state immediately on every navigation change
  • interval: Duration(seconds: 30) - Save state at regular intervals

Using launched() Method #

Call launched() after app initialization to restore previous state or set default routes:

// Restore previous state if available, otherwise use default routes
NavService.instance.launched([NavRouteInfo(path: '/home')]);

// If persisted state exists, it will be restored
// If no persisted state, the provided routes will be used

Manual Persistence Control #

// Manually persist current navigation state
await NavService.instance.persist();

// Manually restore navigation state
await NavService.instance.restore();

Best Practices #

  • Serializable Data Only: Only pass JSON-serializable data in route extras for persistence
  • Coordinate with Auth: Check authentication state before restoring routes
  • Error Handling: Implement fallback logic in onRestore if data is corrupted
  • Performance: Use immediate: true for critical apps or interval for less frequent saves

7. GoRouter Integration #

Setup #

  1. Install dependencies:
dependencies:
  advanced_nav_service: ^0.5.0
  go_router: ^latest_version
  1. Configure both systems:
import 'package:go_router/go_router.dart';
import 'package:advanced_nav_service/nav_service.dart';

final navigatorKey = GlobalKey<NavigatorState>();

// Configure GoRouter
final GoRouter goRouter = GoRouter(
  navigatorKey: navigatorKey,
  observers: [NavService.instance.routeObserver],
  routes: [
    // ... go router routes
  ],
);

void main() {
  // Configure NavService with the same navigator key
  NavService.instance.init(
    NavServiceConfig(
      routes: navServiceRoutes,
      navigatorKey: navigatorKey,
      enableLogger: true,
    ),
  );
  
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: goRouter,
    );
  }
}

Usage with removeAll() #

When switching from NavService to GoRouter navigation, call removeAll() first:

ElevatedButton(
  onPressed: () {
    // Clear NavService stack before using GoRouter
    NavService.instance.removeAll();
    // Then use GoRouter navigation
    context.go('/go-profile/123');
  },
  child: Text('Switch to GoRouter'),
),

Best Practices #

  • Call removeAll() before context.go(): Ensures NavService doesn't interfere with GoRouter
  • Use consistent navigator key: Both systems should share the same GlobalKey<NavigatorState>
  • Include NavService route observer: Add to GoRouter's observers for complete tracking
  • Separate concerns by use case:
    • Use GoRouter for: Static routes, initial redirects, resetting all routes
    • Use NavService for: Dynamic routes, push notifications, unpredictable navigation flows

8. Working with Extra Data #

Passing Data #

NavService.instance.push('/profile', extra: {
  'userId': 123,
  'name': 'John Doe',
  'email': 'john@example.com',
  'preferences': {
    'theme': 'dark',
    'notifications': true,
  },
});

Receiving Data in Screens #

class ProfileScreen extends StatelessWidget {
  final NavState state;
  
  const ProfileScreen({super.key, required this.state});

  @override
  Widget build(BuildContext context) {
    // Access extra data
    final extraData = state.extra?.data ?? {};
    final userId = extraData['userId'];
    final name = extraData['name'];
    
    return Scaffold(
      appBar: AppBar(title: Text('Profile: $name')),
      body: Column(
        children: [
          Text('User ID: $userId'),
          Text('Name: $name'),
          // ... rest of your UI
        ],
      ),
    );
  }
}

9. Navigation History & Debugging #

Accessing Navigation History #

// Get current navigation stack
List<NavStep> history = NavService.instance.navigationHistory;

// Get current location path
String currentLocation = NavService.instance.joinedLocation;

// Print navigation history
for (int i = 0; i < history.length; i++) {
  print('${i + 1}. ${history[i].currentState.path}');
}

The package includes a built-in route observer that automatically tracks navigation events:

MaterialApp(
  navigatorObservers: [NavService.instance.routeObserver],
  // ...
)

10. API Reference #

Main navigation service singleton.

Configuration Methods

  • init(NavServiceConfig config) - Initialize the service with routes and configuration
  • push<T>(String path, {Map<String, dynamic>? extra}) - Push new route
  • pop<T>([T? result]) - Pop current route
  • navigate(String path, {Map<String, dynamic>? extra, bool forcePush = false}) - Intelligent navigation
  • canPop() - Check if can pop

Replace Operations

  • pushReplacement(String path, {Map<String, dynamic>? extra}) - Replace with animation
  • replace(String path, {Map<String, dynamic>? extra}) - Replace without animation

Stack Management

  • pushAndRemoveUntil(String path, RoutePredicate predicate, {Map<String, dynamic>? extra}) - Push and remove until condition
  • popUntilPath(String path) - Pop until specific path
  • removeAll() - Remove all routes without animation

Bulk Operations

  • pushAll(List<NavRouteInfo> routeInfos) - Push multiple routes
  • replaceAll(List<NavRouteInfo> routeInfos) - Replace all routes

Deep Linking

  • openUrl(String url) - Handle deep links via registered link handlers

Persistence

  • launched(List<NavRouteInfo> routes) - Launch app with restored or default routes
  • persist() - Manually persist current navigation state
  • restore() - Manually restore navigation state

Properties

  • navigationHistory - List of navigation steps
  • joinedLocation - Current location path
  • routeObserver - Built-in route observer

Core Classes #

  • NavRoute - Defines a route with path and builder function
  • NavState - Contains route path and extra data for each navigation state
  • NavExtra - Container for extra data passed between routes
  • NavStep - Represents a step in navigation history
  • NavRouteInfo - Simple route information for bulk operations
  • NavServiceConfig - Configuration object for initializing NavService
  • NavLinkHandler - Abstract class for defining deep link handlers (onRedirect requires BuildContext)
  • NavLinkResult - Contains matched route path, path parameters, and query parameters
  • NavPagePersistence - Configuration for navigation state persistence
  • NavPagePersistenceSchedule - Schedule configuration for persistence timing

11. Ultilities #

PageAware #

PageAware is a small utility widget that integrates with the package's built-in RouteObserver to provide easy hooks for common route lifecycle events: initialization, disposal, appearance/disappearance, and a callback after the first frame (optionally waiting for the route transition to complete).

Example usage:

PageAware(
  onInit: () => debugPrint('init'),
  onAfterFirstFrame: () => debugPrint('after first frame'),
  onAppear: () => debugPrint('appeared'),
  onDisappear: () => debugPrint('disappeared'),
  onDispose: () => debugPrint('disposed'),
  waitForTransition: true, // optionally wait for route animation
  child: Scaffold(...),
)

Notes:

  • onInit / onDispose: called during the widget's initState and dispose.
  • onAfterFirstFrame: called after the first frame; if waitForTransition is true, the callback waits until the route's push animation completes.
  • onAppear / onDisappear: called when this route becomes visible or hidden due to navigation events (uses RouteAware hooks).

PageAware is convenient for analytics, lazy-loading content when a screen becomes visible, or coordinating animations that depend on route transitions.

Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

License #

This project is licensed under the BSD-3-Clause License - see the LICENSE file for details.

0
likes
150
points
48
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A comprehensive navigation service package for Flutter applications providing routing, navigation state management, persistence, and navigation utilities.

Repository (GitHub)
View/report issues

Topics

#nav-service #navigation #navigator #router #deep-linking

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on advanced_nav_service