flutter_link_nav
Seamlessly combine Flutter Navigator with Deep Links (Android) & Universal Links (iOS, macOS).
Table of Contents
- Overview
- Features
- Quick Start
- Platform Setup
- Android
- iOS & macOS
- Registering Routes
- Case 1: Single Screen Navigation
- Case 2: Tab Navigation (Multiple Tabs)
- Test Deep Links (Commands)
- Run Examples Locally
- 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.
8. Test Deep Links (Commands)
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 π