🚀 Router Pro · Routing & Lifecycle Awareness Tools

pub package GitHub stars license

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 size
  • visibleBounds: 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

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 (launchMode parameter)
  • Route navigation guards (addRouteGuard method)
  • Named route value return (onResult in pushNamed)
  • Custom 404 page (notFoundPage parameter)

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