flutter_link_nav

Seamlessly combine Flutter Navigator with Deep Links (Android) & Universal Links (iOS, macOS).

Pub.dev GitHub stars Buy Me a Coffee

Table of Contents

  1. Overview
  2. Features
  3. Quick Start
  4. Platform Setup
    • Android
    • iOS & macOS
  5. Registering Routes
  6. Case 1: Single Screen Navigation
  7. Case 2: Tab Navigation (Multiple Tabs)
  8. Test Deep Links (Commands)
  9. Run Examples Locally
  10. Changelog & License

1. Overview

flutter_link_nav helps you:

  • Register routes once and reuse them for both Navigator and deep links.
  • Use deep links to open screens or execute actions (dialogs, sheets, etc.).
  • Support multi-tab screens by mapping tabs via the tab query parameter.

2. Features

  • Global route registry (UI + action).
  • Initial link + link stream handling.
  • Custom deep link handler injection.
  • Tab navigation state sync via deep link events.
  • Null-safe & Flutter 3 compatible.

3. Quick Start

void main() {
  ExampleAppRoutes().registerRoutes();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: globalNavigatorKey,
      initialRoute: ExampleAppRoutes.mainScreen,
      onGenerateRoute: AppRoutes.generateRoute,
    );
  }
}

Call deep link handler inside the first screen you want to receive links:

@override
void initState() {
  super.initState();
  DeepLinkHandler().init(context); // or pass customHandler
}

4. Platform Setup

Android (AndroidManifest.xml):

<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="example.vn" />
</intent-filter>

iOS & macOS (Info.plist):

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array><string>example.vn</string></array>
  </dict>
</array>
<key>NSUserActivityTypes</key>
<array><string>NSUserActivityTypeBrowsingWeb</string></array>

5. Registering Routes

class ExampleAppRoutes extends AppRoutes {
  static const String mainScreen = MainScreen.routeName;
  static const String detailScreen = DetailScreen.routeName;
  static const String tabScreen = TabScreen.routeName; // tab root

  @override
  Map<String, RouteConfig> get routes => {
    mainScreen: RouteConfig(widgetRegister: (query) => const MainScreen()),
    detailScreen: RouteConfig(widgetRegister: (query) => const DetailScreen()),
    'sheet': RouteConfig(actionRegister: (query) async {
      await showDialog(
        context: globalNavigatorKey.currentContext!,
        builder: (_) => AlertDialog(
          title: const Text('Deep Link Detected'),
          content: Text(query['label'] ?? ''),
          actions: [TextButton(onPressed: () => Navigator.of(globalNavigatorKey.currentContext!).pop(), child: const Text('OK'))],
        ),
      );
    }),
    tabScreen: RouteConfig(widgetRegister: (query) => const TabScreen()),
  };
}

6. Case 1: Single Screen Navigation

Minimal screen implementation:

class MainScreen extends StatefulWidget {
  static const String routeName = 'main_screen';
  const MainScreen({super.key});
  @override
  State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
  @override
  void initState() { super.initState(); DeepLinkHandler().init(context); }
  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Main')),
    body: Center(
      child: ElevatedButton(
        onPressed: () => Navigator.pushNamed(context, ExampleAppRoutes.detailScreen),
        child: const Text('Go Detail'),
      ),
    ),
  );
}
class DetailScreen extends StatelessWidget {
  static const String routeName = 'detail_screen';
  const DetailScreen({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Detail')));
}

7. Case 2: Tab Navigation (Multiple Tabs)

Entry point for tab-based example chooses TabScreen as initial route.

void main() { ExampleAppRoutes().registerRoutes(); runApp(const MyApp()); }
class MyApp extends StatelessWidget { /* same as Quick Start but initialRoute = tabScreen */ }

Tab screen with deep link aware tab switching:

class TabScreen extends StatefulWidget {
  static const String routeName = 'main'; // root for tab deep links
  const TabScreen({super.key, this.route});
  final String? route;
  @override State<TabScreen> createState() => _TabScreenState();
}
class _TabScreenState extends State<TabScreen> {
  int _selectedIndex = 0;
  final pages = [const HomePage(), const SearchPage(), const ProfilePage()];
  int mapTab(String? route) => switch (route) { 'search' => 1, 'profile' => 2, _ => 0 };
  @override
  void initState() {
    super.initState();
    _selectedIndex = mapTab(widget.route);
    DeepLinkHandler().init(
      context,
      customHandler: (ctx, uri) => ctx.handleNavigationOnTab(
        uri,
        config: TabNavigationConfig(
          getTabIndex: mapTab,
          currentTabIndex: _selectedIndex,
          updateTabIndex: (i) => setState(() => _selectedIndex = i),
        ),
      ),
    );
  }
  @override
  Widget build(BuildContext context) => Scaffold(
    body: IndexedStack(index: _selectedIndex, children: pages),
    bottomNavigationBar: BottomNavigationBar(
      currentIndex: _selectedIndex,
      onTap: (i) => setState(() => _selectedIndex = i),
      items: const [
        BottomNavigationBarItem(icon: Icon(Icons.home_outlined), label: 'Home'),
        BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
        BottomNavigationBarItem(icon: Icon(Icons.person_outline), label: 'Profile'),
      ],
    ),
  );
}

Required Input Parameters (must provide all):

  • getTabIndex(String? tabRoute) -> int : Map route token to index.
  • currentTabIndex : Current selected tab index.
  • updateTabIndex(int) : Perform UI tab change. Examples:
  • example.vn://main?tab=search → Switch to Search.
  • example.vn://main?tab=profile → Switch to Profile.
  • example.vn://main → Reset/stay Home.

Android:

adb shell am start -W -a android.intent.action.VIEW -d "example.vn://detail_screen" com.example.example
adb shell am start -W -a android.intent.action.VIEW -d "example.vn://main?tab=search" com.example.example

iOS Simulator:

xcrun simctl openurl DEVICE_ID "example.vn://detail_screen"
xcrun simctl openurl DEVICE_ID "example.vn://main?tab=profile"

macOS:

open "example.vn://detail_screen"
open "example.vn://main?tab=search"

Replace DEVICE_ID via flutter devices or xcrun simctl list.

9. Run Examples Locally

Case 1 (Single Screen):

flutter run -t example/lib/case_normal/main.dart -d android
flutter run -t example/lib/case_normal/main.dart -d ios
flutter run -t example/lib/case_normal/main.dart -d macos

Case 2 (Tabs):

flutter run -t example/lib/case_multiple_tab_screen/tab_screen.dart -d android
flutter run -t example/lib/case_multiple_tab_screen/tab_screen.dart -d ios
flutter run -t example/lib/case_multiple_tab_screen/tab_screen.dart -d macos

10. Changelog & License

  • See CHANGELOG.md for version history.
  • Licensed under the terms in LICENSE.

Tips

  • Use AppRoutes.executeRouteAction('sheet', arguments: {...}); for non-navigation actions.
  • Avoid pushing the same route without params; handler already skips.
  • For debugging: add debugPrint(uri.toString()); in custom handler.

Next Ideas (Contributions Welcome)

  • Add web platform support.
  • Add guard/middleware before navigation.
  • Provide a built-in tab mapping helper.

Enjoy building with deep links 🚀


🤝 Contributing

Contributions, issues, and feature requests are welcome!
Feel free to check the issues page.

Show your support

If you find this project useful and it helps you save time, please consider supporting it. Your support keeps me motivated to maintain and improve this package!

Buy Me a Coffee

Developed with ❤️ by Trung Ngo

GitHub Pub.dev

Libraries