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

A comprehensive navigation service package for Flutter applications providing routing, navigation state management, 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: Active development for this project will continue under a new package: https://pub.flutter-io.cn/packages/flutter_nav β€” the advanced_nav_service package will still be maintained.

Table of Contents #

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

1. Installation #

Add this package to your pubspec.yaml:

dependencies:
  advanced_nav_service: ^0.4.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
  • 🧰 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 LaunchScreen(),
    );
  }
}

4. LaunchScreen (handle initial logic) #

Use a dedicated LaunchScreen that runs initial checks in initState (authentication, initial push notification, onboarding, etc.) and then redirects with NavService. Keep logic in a single async method called from initState to avoid making initState async.

import 'package:flutter/material.dart';
import 'package:advanced_nav_service/nav_service.dart';

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

  @override
  State<LaunchScreen> createState() => _LaunchScreenState();
}

class _LaunchScreenState extends State<LaunchScreen> {
  @override
  void initState() {
    super.initState();
    // Defer async work to a helper; run after first frame
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _handleInitialLogic();
    });
  }

  Future<void> _handleInitialLogic() async {
    // 1) Check if the user is authenticated
    final bool isAuthenticated = await _checkAuth();
    // 2) Check if app was opened from a push notification / deep link
    // Replace with your push/deep-link SDK fetch (e.g., FirebaseMessaging.getInitialMessage())
    final Uri? initialDeepLink = await _getInitialDeepLink();

    // Decide target route
    if (initialDeepLink != null) {
      // Convert deep link to a route or call NavService.openUrl
      NavService.instance.openUrl(initialDeepLink.toString());
      return;
    }

    if (!isAuthenticated) {
      // Redirect to login or onboarding
      NavService.instance.replaceAll([
        NavRouteInfo(path: '/login', extra: {}), 
        // ...
      ]);
      return;
    }

    // Default: go to home
    NavService.instance.pushReplacement('/home');
  }

  // Dummy implementations - replace with real logic
  Future<bool> _checkAuth() async {
    // e.g., await authService.isLoggedIn();
    await Future.delayed(const Duration(milliseconds: 200));
    return false; // change to actual auth result
  }

  Future<Uri?> _getInitialDeepLink() async {
    // e.g., final message = await FirebaseMessaging.instance.getInitialMessage();
    // if (message != null) parse message.data or message.link
    await Future.delayed(const Duration(milliseconds: 50));
    return null; // return a Uri if the app was opened via push/deep-link
  }

  @override
  Widget build(BuildContext context) {
    // Can define as a plash screen
    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(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(NavLinkResult result) {
    NavService.instance.navigate('/settings', extra: {
      ...result.pathParameters,
      ...result.queryParameters,
    });
  }
}
  1. Install dependencies:
dependencies:
  advanced_nav_service: ^0.4.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 use a `LaunchScreen` that already handles initial logic
// (see "LaunchScreen (handle initial logic)" above), prefer handling the
// initial deep link inside that screen's `_handleInitialLogic()` to avoid
// duplicate navigation. Use either `LaunchScreen` or `_initializeAppLinks()`
// for initial link handling β€” not both.

See LaunchScreen for more details.

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. GoRouter Integration #

Setup #

  1. Install dependencies:
dependencies:
  advanced_nav_service: ^0.4.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

7. 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
        ],
      ),
    );
  }
}

8. 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],
  // ...
)

9. 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

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
  • NavLinkResult - Contains matched route path, path parameters, and query parameters

10. 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
160
points
377
downloads

Publisher

unverified uploader

Weekly Downloads

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

Repository (GitHub)
View/report issues

Topics

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

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on advanced_nav_service