🚀 Router Pro · Routing & Lifecycle Awareness Tools
Language: English | 简体中文
✨ Overview
Router Pro is a powerful Flutter routing framework that combines Navigator 2.0 with lifecycle awareness, providing Android-like lifecycle capabilities and advanced routing features.
🎯 Core Features
🔄 Advanced Routing
- Launch Modes: Standard, SingleTop, SingleInstance (like Android)
- Route Guards: Intercept navigation for permission checks
- Named Routes: Support path parameters (
:id) and query parameters - Value Return: Pass data back from routes
- Drawer Router Stack: Independent routing in drawers
- 404 Handling: Custom error pages
⏱️ Lifecycle Awareness
- Native-like Lifecycle:
onCreate,onResume,onPause,onDestroy - Visibility Detection: Track widget visibility in lists
- Lazy Loading: Load content when visible
- Auto Play/Pause: Control videos based on visibility
🎨 Developer Experience
- No BuildContext Required: Navigate without context
- Type Safe: Full Dart type safety
- Decoupled Design: Use routing and lifecycle independently
- Cross Platform: Works on App & Web
🌟 Why Router Pro?
Flutter's StatelessWidget/StatefulWidget doesn't natively support lifecycle callbacks like Android's Activity/Fragment. This makes it challenging to:
- Pause/resume videos when pages are hidden
- Load data only when pages are visible
- Clean up resources when pages are destroyed
Router Pro solves these problems by providing a complete routing and lifecycle solution that feels natural to Android developers while embracing Flutter's declarative style.
📦 Installation
Add to pubspec.yaml:
dependencies:
router_pro: ^0.2.0
Import:
import 'package:router_pro/router_lib.dart';
⚡ Function 1: RouterProxy
global registration
RouterProxy router = RouterProxy.getInstance(
pageMap: {'/': const Login()},
exitWindow: _confirmExit,
notFoundPage: const NotFoundPage(), // Optional: Custom 404 page
);
MaterialApp.router(
routerDelegate: router,
routeInformationParser: router.defaultParser(),
);
page skip
router.push(); //Jump to page widget method
router.pushNamed(); //Jump page routing path method
router.replace(); //Replace current page
router.popAndPushNamed(); //Pop the current page, then push a new page
router.pushAndRemoveAll();//Empty the page stack and push new page
router.pushNamedAndRemoveAll();// Skip to the specified page and clear all previous pages
router.pushStackTop();//Place the page at the top of the stack (remove it first if it already exists)
//example:
router.push(
page:const TaoBaoPageDetail(),
launchMode: LaunchMode.standard, // Optional: Launch mode
onResult: (value){ //optional
setState((){
title = "Taobao page$value";
});
});
router.pushNamed(
name: '/TaoBaoPageDetail',
launchMode: LaunchMode.singleTop, // Optional: Launch mode
onResult: (value){ //optional: Support named route value return
setState((){
title = "Taobao page$value";
});
});
Route Launch Modes
Supports three launch modes, fully simulating Android Activity launch mode behavior:
// 1. Standard mode (default) - Allows multiple instances of the same page
router.push(
page: const DetailPage(),
launchMode: LaunchMode.standard,
);
// 2. Single Top - If the target page is already at the top, updates parameters instead of creating new instance
// Similar to Android's singleTop mode, triggers page rebuild (like onNewIntent)
router.push(
page: DetailPage(title: 'New Title'),
launchMode: LaunchMode.singleTop,
);
// 3. Single Instance - Only one instance in the entire stack
// If exists, clears all pages above it and updates parameters (like Android's singleInstance + onNewIntent)
router.push(
page: ShoppingCartPage(itemCount: 5),
launchMode: LaunchMode.singleInstance,
);
Comparison of Three Modes
| Mode | At Stack Top | Elsewhere in Stack | Update Params | Clear Above | Typical Use Cases |
|---|---|---|---|---|---|
| Standard | Create new | Create new | ❌ | ❌ | Detail pages, Forms |
| SingleTop | Update & rebuild | Create new | ✅ | ❌ | Search, Notifications |
| SingleInstance | Update & rebuild | Clear above & update | ✅ | ✅ | Cart, Home, Player |
Detailed Behavior
Scenario 1: Stack A -> B -> C, launch C again
| Mode | Result Stack | Description |
|---|---|---|
| Standard | A -> B -> C -> C | Create new C instance |
| SingleTop | A -> B -> C (updated) | C at top, update params |
| SingleInstance | A -> B -> C (updated) | C at top, update params |
Scenario 2: Stack A -> B -> C -> D, launch C again
| Mode | Result Stack | Description |
|---|---|---|
| Standard | A -> B -> C -> D -> C | Create new C instance |
| SingleTop | A -> B -> C -> D -> C | C not at top, create new |
| SingleInstance | A -> B -> C (updated) | Clear D, update C |
Scenario 3: Stack A -> B -> C -> D -> E, launch B again
| Mode | Result Stack | Description |
|---|---|---|
| Standard | A -> B -> C -> D -> E -> B | Create new B instance |
| SingleTop | A -> B -> C -> D -> E -> B | B not at top, create new |
| SingleInstance | A -> B (updated) | Clear C, D, E, update B |
Mode Details
Standard Mode
- Default mode, creates new instance each time
- Allows multiple instances of the same page
- Use cases: Detail pages, forms, search results, chat pages
SingleTop Mode
- If target page is at stack top, updates parameters and rebuilds (like Android
onNewIntent) - If target page is not at top, creates new instance
- Use cases: Search pages, notification clicks, deep links, scan result pages
- Note: Page will rebuild, internal state will be lost
SingleInstance Mode
- Globally unique instance, only one instance of the page in entire app
- If page exists, clears all pages above it and updates with new parameters
- Use cases: Shopping cart, home page, player, notification center, settings
- Note: Clears pages above, may affect user navigation experience
Usage Examples
// Example 1: Shopping Cart (SingleInstance)
void addToCart(Product product) {
cartItems.add(product);
router.push(
page: ShoppingCartPage(itemCount: cartItems.length, items: cartItems),
launchMode: LaunchMode.singleInstance,
);
// Returns to the same cart from any page
// Clears all pages above the cart
}
// Example 2: Search Page (SingleTop)
void search(String keyword) {
router.push(
page: SearchResultPage(keyword: keyword),
launchMode: LaunchMode.singleTop,
);
// Multiple searches update results instead of creating multiple pages
}
// Example 3: Product Detail (Standard)
void viewProduct(String productId) {
router.push(
page: ProductDetailPage(productId: productId),
launchMode: LaunchMode.standard,
);
// Users can open multiple product details for comparison
}
Page close & return
router.pop(); //Close the current page
router.popWithResult(); //Close the current page, mainly used for (dialog, Bottom Sheet..)
example:
router.popWithResult("return value:hello"); //Return value optional
router.pop("return value:hello"); //Return value optional
Pop-up windows without context
router.showAppBottomSheet()
router.showAppDialog()
router.showAppSnackBar()
example:
router.showAppBottomSheet(builder: (context){
return Container(
height: 400,
width: MediaQuery.of(context).size.width,
color: Colors.red,
child: GestureDetector(
onTap: (){
router.popWithResult("This is the return result");
},
child: const Text('Click on me to get the return value of Bottom Sheet'),
),
);
}).then((value){
debugPrint("showAppBottomSheet value:${value}");
});
Route Navigation Guards
Support two types of route interception for permission verification, login checks, etc.:
Method 1: Named Route Guard (for pushNamed)
// Add route guard
router.addRouteGuard((from, to) async {
// Pages that require login
final protectedRoutes = ['/profile', '/settings'];
final isLoggedIn = await checkLoginStatus();
if (protectedRoutes.contains(to.uri.toString()) && !isLoggedIn) {
// Intercept navigation, redirect to login page
router.pushNamed(name: '/login');
return false; // Return false to block navigation
}
return true; // Return true to allow navigation
});
// Use named route navigation
router.pushNamed(name: '/profile');
Method 2: Page Type Guard (for push(page: xxx))
// Add page type guard
router.addPageTypeGuard((fromPageType, toPageType) async {
// Page types that require login
final protectedPageTypes = [
ProfilePage,
SettingsPage,
AutoPlayVideoExample,
];
final isLoggedIn = await checkLoginStatus();
if (protectedPageTypes.contains(toPageType) && !isLoggedIn) {
// Intercept navigation, redirect to login page
router.pushNamed(name: '/login');
return false; // Return false to block navigation
}
return true; // Return true to allow navigation
});
// Use page instance navigation
router.push(page: ProfilePage());
Method 3: Hybrid Approach (supports both guards)
// Add name parameter to push method to trigger both guards
router.push(
page: ProfilePage(),
name: '/profile', // Add name parameter for route guard recognition
);
Guard Management
// Remove guards
router.removeRouteGuard(guard);
router.removePageTypeGuard(pageTypeGuard);
// Clear all guards
router.clearRouteGuards();
router.clearPageTypeGuards();
404 Error Page
Support custom error page for undefined routes:
RouterProxy router = RouterProxy.getInstance(
pageMap: {'/': const HomePage()},
notFoundPage: const Custom404Page(), // Custom 404 page
);
// When accessing a non-existent route, the 404 page will be displayed
router.pushNamed(name: '/not-exist');
⚡ Feature 3: Enhanced Route Parser
Basic Parser (CustomParser)
Default simple parser that does no parsing:
MaterialApp.router(
routerDelegate: router,
routeInformationParser: router.defaultParser(), // CustomParser
);
Enhanced Parser (EnhancedParser)
Supports advanced features like path parameters, query parameters, and route aliases:
final parser = EnhancedParser(
enablePathParams: true, // Enable path parameter parsing
enableQueryParams: true, // Enable query parameter parsing
routeAliases: { // Route aliases
'/home': '/',
'/profile': '/user/me',
},
patterns: [ // Route patterns
RoutePattern('/user/:id'),
RoutePattern('/product/:category/:id'),
RoutePattern('/posts/:year/:month/:day'),
],
);
MaterialApp.router(
routerDelegate: router,
routeInformationParser: parser,
);
Path Parameter Parsing
Supports :param format path parameters:
// Define route patterns
final parser = EnhancedParser(
patterns: [
RoutePattern('/user/:id'),
RoutePattern('/product/:category/:id'),
],
);
// Get parameters in routePathCallBack
RouterProxy.getInstance(
routePathCallBack: (routeInfo) {
final params = RouteParams.fromState(routeInfo.state);
if (params?.matchedPattern == '/user/:id') {
final userId = params!.getPathParam('id');
return UserDetailPage(userId: userId!);
}
return null;
},
);
// Usage
router.pushNamed(name: '/user/123');
// Parsed result: {id: '123'}
Query Parameter Parsing
Automatically parses URL query parameters:
// URL: /search?q=Flutter&page=2
final parser = EnhancedParser(
enableQueryParams: true,
);
// Get parameters in routePathCallBack
RouterProxy.getInstance(
routePathCallBack: (routeInfo) {
final params = RouteParams.fromState(routeInfo.state);
if (routeInfo.uri.path == '/search') {
final keyword = params?.getQueryParam('q');
final page = params?.getQueryParam('page');
return SearchResultPage(keyword: keyword!, page: int.parse(page!));
}
return null;
},
);
// Usage
router.pushNamed(name: '/search?q=Flutter&page=2');
// Parsed result: {q: 'Flutter', page: '2'}
Route Aliases
Define aliases for routes:
final parser = EnhancedParser(
routeAliases: {
'/home': '/',
'/profile': '/user/me',
'/settings': '/user/settings',
},
);
// Navigate using aliases
router.pushNamed(name: '/home'); // Actually navigates to /
router.pushNamed(name: '/profile'); // Actually navigates to /user/me
Combined Usage
Path parameters and query parameters can be used together:
// URL: /product/electronics/123?color=red&size=large
final parser = EnhancedParser(
enablePathParams: true,
enableQueryParams: true,
patterns: [
RoutePattern('/product/:category/:id'),
],
);
// Parsed result:
// pathParams: {category: 'electronics', id: '123'}
// queryParams: {color: 'red', size: 'large'}
Use Cases
1. Web Deep Linking
// User directly visits: https://example.com/product/electronics/123?color=red
// Automatically parses parameters and displays corresponding page
2. Share Links
// Generate share link
final shareUrl = 'https://example.com/product/electronics/123';
// User clicks link and automatically navigates to product detail page
3. Notification Navigation
// Notification carries deep link
void handleNotification(String deepLink) {
// deepLink: /user/123?tab=posts
router.pushNamed(name: deepLink);
// Automatically parses and navigates to user page posts tab
}
4. SEO Optimization
// Friendly URL structure
// /blog/2024/01/15/my-post-title
// Instead of /blog?id=123
⚡ Feature 4: Drawer Router Stack
Support creating independent router stacks within drawers, each with complete routing functionality.
Core Features
- ✅ Independent Router Stack: Drawer has its own page stack, doesn't affect main router
- ✅ Auto Refresh: Automatically updates drawer display on push/pop
- ✅ No Context Dependency: Uses GlobalKey to manage drawer state, avoiding context binding issues
- ✅ Complete Features: Supports route guards, launch modes, value return, etc.
- ✅ Multi-instance Support: Can create multiple independent drawer router stacks
Basic Usage
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 1. Create GlobalKey
final scaffoldKey = GlobalKey<ScaffoldState>();
late final DrawerStackController controller;
@override
void initState() {
super.initState();
// 2. Create DrawerStackController with GlobalKey
controller = DrawerStackController(
scaffoldKey: scaffoldKey,
routerProxy: RouterProxy.getDrawerInstance(
stackId: 'main-drawer',
pageMap: {
'/': DrawerHomePage(),
'/settings': DrawerSettingsPage(),
},
),
config: DrawerConfig(
autoOpen: true, // Auto open drawer on first push
autoClose: true, // Auto close drawer when stack is empty
isEndDrawer: true, // Right drawer
),
);
}
@override
void dispose() {
// Clean up resources
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey, // 3. Pass GlobalKey to Scaffold
appBar: AppBar(title: Text('Home')),
// 4. Use DrawerNavigator with custom styling
endDrawer: Container(
width: 300,
color: Colors.white,
child: DrawerNavigator(controller: controller),
),
body: ElevatedButton(
onPressed: () {
// 5. Direct call, no context needed
controller.push(page: DrawerSettingsPage());
},
child: Text('Open Drawer Settings'),
),
);
}
}
Drawer Pages
Access the controller inside drawer pages using InheritedDrawerStackController:
class DrawerHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Get controller from context
final controller = InheritedDrawerStackController.of(context);
return Column(
children: [
AppBar(
title: Text('Drawer Menu'),
actions: [
IconButton(
icon: Icon(Icons.close),
onPressed: () => controller?.closeDrawer(),
),
],
),
ListTile(
leading: Icon(Icons.settings),
title: Text('Settings'),
onTap: () => controller?.push(page: DrawerSettingsPage()),
),
],
);
}
}
Drawer Control Methods
// DrawerStackController methods
controller.openDrawer(); // Open drawer
controller.closeDrawer(); // Close drawer
controller.isDrawerOpen; // Check if drawer is open
// Navigation methods (same as RouterProxy)
controller.push(page: SettingsPage());
controller.pushNamed(name: '/settings');
controller.pop();
// Main router stack methods (for controlling drawer from main page)
router.openMainDrawer(isEndDrawer: true); // Open right drawer
router.closeMainDrawer(isEndDrawer: false); // Close left drawer
router.isMainDrawerOpen(isEndDrawer: true); // Check if right drawer is open
⚡ Feature 5: Lifecycle Awareness
Make StatelessWidget / StatefulWidget have onResume / onPause / onDestroy ability:
class Login extends StatelessWidget {
const Login({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return LifeCycle(
onCreate: () => print("Login onCreate"),
onStart: () => print("Login onStart"),
onResume: () => print("Login onResume"),
onPause: () => print("Login onPause"),
onDestroy: () => print("Login onDestroy"),
child: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => router.push(page: NavPage()),
child: const Text("Login"),
),
),
),
);
}
}
Advanced Features
Visibility Threshold: Customize the visibility percentage to trigger onResume
LifeCycle(
visibilityThreshold: 0.5, // Trigger onResume at 50% visible, default 1.0 (fully visible)
onResume: () => print('Page 50% visible'),
child: YourWidget(),
)
Debug Mode: Output lifecycle logs
LifeCycle(
debugLabel: 'HomePage', // Enable debug logging
onCreate: () => print('Created'),
onResume: () => print('Visible'),
child: YourWidget(),
)
// Output: [LifeCycle:HomePage] onCreate
// Output: [LifeCycle:HomePage] onResume (widget visible: 100.0%)
⚡ Function 6: Visibility Detection
Low-level visibility detection component for precise visibility monitoring.
Basic Usage
VisibilityDetector(
key: Key('my-widget'),
onVisibilityChanged: (info) {
print('Visible fraction: ${info.visibleFraction}');
// Use convenience properties
if (info.isFullyVisible) {
print('Fully visible');
} else if (info.isPartiallyVisible) {
print('Partially visible');
} else if (info.isInvisible) {
print('Invisible');
}
},
child: YourWidget(),
)
Use Cases
1. Lazy Loading Images
VisibilityDetector(
key: Key('image-$index'),
onVisibilityChanged: (info) {
if (info.visibleFraction >= 0.5 && !imageLoaded) {
loadImage(); // Load when 50% visible
}
},
child: Image.network(imageUrl),
)
2. Auto-play Videos
VisibilityDetector(
key: Key('video-$index'),
onVisibilityChanged: (info) {
if (info.visibleFraction >= 0.8) {
videoController.play(); // Play when 80% visible
} else if (info.isInvisible) {
videoController.pause(); // Pause when invisible
}
},
child: VideoPlayer(videoController),
)
3. Exposure Tracking
VisibilityDetector(
key: Key('item-$index'),
onVisibilityChanged: (info) {
if (info.isFullyVisible) {
trackExposure(itemId); // Track when fully visible
}
},
child: ProductCard(product),
)
4. List Item Visibility Monitoring
ListView.builder(
itemBuilder: (context, index) {
return VisibilityDetector(
key: Key('list-item-$index'),
onVisibilityChanged: (info) {
print('Item $index: ${(info.visibleFraction * 100).toInt()}% visible');
},
child: ListTile(title: Text('Item $index')),
);
},
)
VisibilityInfo Properties
visibleFraction: Visible fraction (0.0 - 1.0)isVisible: Whether visible (> 0%)isInvisible: Whether invisible (0%)isFullyVisible: Whether fully visible (100%)isPartiallyVisible: Whether partially visible (0% - 100%)size: Widget sizevisibleBounds: Visible bounds
Controller Configuration
// Set update interval
VisibilityDetectorController.instance.updateInterval = Duration(milliseconds: 300);
// Trigger all callbacks immediately
VisibilityDetectorController.instance.notifyNow();
// Clear callbacks for specific widget
VisibilityDetectorController.instance.forget(Key('my-widget'));
// Get widget bounds
final bounds = VisibilityDetectorController.instance.widgetBoundsFor(Key('my-widget'));
📖 Usage Examples
Example 1: Login Guard
void initRouter() {
final router = RouterProxy.getInstance(
pageMap: {'/': HomePage(), '/login': LoginPage(), '/profile': ProfilePage()},
);
// Add login verification guard
router.addRouteGuard((from, to) async {
final protectedRoutes = ['/profile', '/settings', '/orders'];
if (protectedRoutes.contains(to.uri.toString())) {
final isLoggedIn = await checkLoginStatus();
if (!isLoggedIn) {
router.pushNamed(name: '/login');
return false;
}
}
return true;
});
}
Example 2: Video Player Lifecycle
class VideoPlayerPage extends StatefulWidget {
@override
_VideoPlayerPageState createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State<VideoPlayerPage> {
late VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.network('video_url');
}
@override
Widget build(BuildContext context) {
return LifeCycle(
visibilityThreshold: 0.8, // Play when 80% visible
debugLabel: 'VideoPlayer', // Debug logging
onResume: () => _controller.play(), // Play when visible
onPause: () => _controller.pause(), // Pause when invisible
onDestroy: () => _controller.dispose(), // Release resources
child: Scaffold(
body: VideoPlayer(_controller),
),
);
}
}
Example 3: Shopping Cart Single Instance
// Shopping cart page maintains only one instance in the entire app
router.push(
page: const ShoppingCartPage(),
launchMode: LaunchMode.singleInstance,
);
Example 4: Custom 404 Page
class NotFoundPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Page Not Found')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 100, color: Colors.red),
SizedBox(height: 20),
Text('404', style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
Text('Sorry, the page you are looking for does not exist'),
SizedBox(height: 30),
ElevatedButton(
onPressed: () => router.goRootPage(),
child: Text('Back to Home'),
),
],
),
),
);
}
}
📋 Quick Reference
Navigation
router.push(page: MyPage()); // Widget navigation
router.pushNamed(name: '/page'); // Named route navigation
router.push(page: MyPage(), launchMode: LaunchMode.singleTop); // Single top
router.push(page: MyPage(), launchMode: LaunchMode.singleInstance); // Single instance
Close Routes
router.pop(); // Close current page
router.pop('return data'); // Close with data
router.popWithResult('return data'); // For Dialog/BottomSheet
Route Guards
router.addRouteGuard((from, to) async {
// Return true to allow, false to block
return true;
});
Dialogs
router.showAppDialog(builder: (ctx) => AlertDialog(...));
router.showAppBottomSheet(builder: (ctx) => Container(...));
router.showAppSnackBar(message: 'Message');
Lifecycle
// Basic usage
LifeCycle(
onResume: () => print('Page visible'),
onPause: () => print('Page invisible'),
onDestroy: () => print('Page destroyed'),
child: YourWidget(),
)
// Advanced usage
LifeCycle(
visibilityThreshold: 0.5, // Trigger at 50% visible
debugLabel: 'MyPage', // Debug logging
onResume: () => print('Visible'),
child: YourWidget(),
)
🔄 Migration Guide
Upgrading from 0.1.x to 0.2.0
Version 0.2.0 is fully backward compatible. All existing code works without modifications.
New Features (optional):
- Route launch modes (
launchModeparameter) - Route navigation guards (
addRouteGuardmethod) - Named route value return (
onResultinpushNamed) - Custom 404 page (
notFoundPageparameter)
Upgrade Steps:
# Update pubspec.yaml
dependencies:
router_pro: ^0.2.0
flutter pub get
📝 Complete Example
See example/lib/main.dart for complete example code, including:
- ✅ All route launch mode demonstrations
- ✅ Named route value return
- ✅ Route guard interception
- ✅ 404 error page
- ✅ Lifecycle awareness
- ✅ Visibility detection use cases
Run example:
cd example
flutter run
🛠 other explanatory
ExitWindow: Customizable prompt box to exit the program- Web side: Support browser direct access, need to customize
RouteParser - Complete API documentation: See source code comments
📄 Changelog
v0.2.0
- ✅ Added route launch modes (Standard, SingleTop, SingleInstance)
- ✅ Added route navigation guards
- ✅ Added named route value return support
- ✅ Added custom 404 error page
- ✅ Added lifecycle visibility threshold (visibilityThreshold)
- ✅ Added lifecycle debug mode (debugLabel)
- ✅ Added VisibilityInfo convenience properties (isVisible, isInvisible, isFullyVisible, isPartiallyVisible)
- ✅ Optimized route stack management
- ✅ Improved visibility detection logic
- ✅ Enhanced documentation with complete examples
v0.1.1
- Router proxy functionality
- Lifecycle awareness
- Context-free dialogs support
MIT License © zhengzaihong