tp_router 0.6.2 copy "tp_router: ^0.6.2" to clipboard
tp_router: ^0.6.2 copied to clipboard

discontinuedreplaced by: teleport_router

A simplified Flutter router based on go_router with annotation support.

TpRouter #

中文文档

Package Version
tp_router pub package
tp_router_annotation pub package
tp_router_generator pub package

A simplified, type-safe, and annotation-driven routing library for Flutter.

TpRouter is built on top of go_router—the official Flutter routing package. This means you get all the battle-tested features (deep linking, web support, nested navigation) without worrying about core stability. TpRouter simply provides a more ergonomic, annotation-based API that eliminates boilerplate and enables type-safe navigation.


Table of Contents #


✨ Features #

  • 🗝️ NavKey-Driven Linking: No more nesting hell. Just tell a route "My parent is MainNavKey", and they are automatically linked.
  • 📐 Type-Safe Navigation: UserRoute(id: 1).tp() instead of string manipulation.
  • 🐚 Simple Shells: Define app layouts (BottomNav, Drawers) purely through annotations.
  • 🛡️ Type-Safe Guards: Strongly-typed TpRedirect<T> for route protection.
  • 🔄 Reactive Routing: Use refreshListenable to auto-redirect on state changes.
  • 🧩 Smart Code Gen: Automatically handles parameters, return values, and deep linking.

🧩 Core Concepts #

Understanding how TpRouter works helps you leverage its full power.

1. The Triad of Navigation #

TpRouter connects three key pieces:

  • Routes (@TpRoute): Static configuration of what screens you have.
  • Generator: Converts annotations into strongly-typed classes (UserRoute, HomeRoute).
  • Router (TpRouter): The runtime engine that manages the navigation stack using go_router.

2. TpNavKey: The Bridge #

TpNavKey is more than just a GlobalKey. It is the binding agent that connects:

  • A Shell (UI container)
  • A Navigator (Flutter's navigation stack)
  • An Observer (TpRouter's tracking system)

When you define class MainKey extends TpNavKey, you are creating a unique identifier that ensures your ShellRoute uses the exact same navigator instance that your routes are trying to navigate into.

3. Smart Observation #

TpRouter automatically injects TpRouteObserver into every navigator managed by a TpNavKey (especially in ShellRoutes). This observer tracks the live route stack, enabling advanced features like:

  • popUntil(predicate)
  • popToInitial()
  • removeWhere()

Normal go_router doesn't easily support these because it manages URLs, not Flutter Route objects. TpRouter bridges this gap by watching the actual Navigator activities.

4. Architecture Deep Dive #

TpRouter is designed as a compile-time abstraction layer over go_router.

Layer Component Role
User Code Annotations (@TpRoute) Define the navigation structure and parameters declaratively.
Build System tp_router_generator Analyzes code and generates type-safe Route classes.
Runtime TpRouteData The common interface for all routes. It unifies parameters (path, query, extra) into a single API.
Core TpRouter A singleton wrapper adjusting go_router configuration and managing global state.
Engine go_router Handles URL parsing, deep linking, and low-level navigation.

Data Flow:

  1. Code Gen: Annotated UserPage(id) becomes UserRoute(id).
  2. Navigation: Calling UserRoute(id: 123).tp() converts the object into a URL path (/user/123) and extra data.
  3. Routing: TpRouter tells go_router to navigate.
  4. Reconstruction: When the page builds, TpRouter uses TpRouteData.of(context) to parse the URL/Web State back into usable data.

Installation #

Add the following to your pubspec.yaml:

dependencies:
  tp_router: ^0.5.1
  tp_router_annotation: ^0.5.0

dev_dependencies:
  build_runner: ^2.4.0
  tp_router_generator: ^0.5.0

Run the generator:

dart run build_runner build

🚀 Quick Start #

1. Define NavKeys #

NavKeys are the heart of TpRouter. They act as unique identifiers for your navigators and bridges between parents and children.

Create a file lib/routes/nav_keys.dart:

import 'package:tp_router/tp_router.dart';

// Key for the main application shell (e.g. BottomNavigationBar)
class MainNavKey extends TpNavKey {
  const MainNavKey() : super('main');
}

// Sub-keys for branches if you use IndexedStack (optional but recommended)
class HomeNavKey extends TpNavKey {
  const HomeNavKey() : super('main', branch: 0);
}

class SettingsNavKey extends TpNavKey {
  const SettingsNavKey() : super('main', branch: 1);
}

2. Define Shells #

Mark your container widget (e.g., a page with BottomNavigationBar) with @TpShellRoute. Link it to a key (MainNavKey).

@TpShellRoute(
  navigatorKey: MainNavKey, // <--- Identified by this Key
  isIndexedStack: true,     // Enable stateful nested navigation
  branchKeys: [HomeNavKey, SettingsNavKey], // <--- Define branch key types (not instances)
)
class MainShellPage extends StatelessWidget {
  final TpStatefulNavigationShell navigationShell;
  const MainShellPage({required this.navigationShell, super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: navigationShell.currentIndex,
        onTap: (index) => navigationShell.tp(index),
        items: [/* ... */],
      ),
    );
  }
}

3. Define Routes #

Just annotate your pages.

  • To nest a page, simply set parentNavigatorKey to the Shell's key.
  • No nesting? Omit the key.
// Standard Route (not nested)
@TpRoute(path: '/login')
class LoginPage extends StatelessWidget { ... }

// Nested Route (Child of MainShellPage)
@TpRoute(
  path: '/home',
  isInitial: true,
  parentNavigatorKey: HomeNavKey, // <--- Linked to MainShell's branch 0 automatically!
)
class HomePage extends StatelessWidget { ... }

// Another Nested Route
@TpRoute(
  path: '/settings',
  parentNavigatorKey: SettingsNavKey, // <--- Linked to MainShell's branch 1
)
class SettingsPage extends StatelessWidget { ... }

4. Initialize Router #

Pass the generated tpRoutes to TpRouter.

import 'routes/route.gr.dart';

void main() {
  final router = TpRouter(
    routes: tpRoutes, // Generated by build_runner
  );

  runApp(MaterialApp.router(
    routerConfig: router.routerConfig,
  ));
}

🧭 Navigation #

Type-Safe Navigation #

The generator creates a Route class for every annotated widget.

// Push a new route
UserRoute(id: 123).tp();

// Await a result
final result = await SelectProfileRoute().tp<String>();

// Replace current route (no back navigation)
LoginRoute().tp(replacement: true);

// Clear history and go (like go_router's `go`)
HomeRoute().tp(clearHistory: true);

Pop & Stack Control #

// Pop topmost route
context.pop();
// or with result
context.pop(result: 'selected_item');

// --- Using context extensions (Recommended) ---

// Access TpRouter helper via context
context.tpRouter.popTo(HomeRoute());

context.tpRouter.popToInitial();

// --- Using Static Instance (Global) ---

// Pop until a specific route
TpRouter.instance.popTo(HomeRoute());

// Pop to initial route
TpRouter.instance.popToInitial();

// Remove a specific route from stack (without navigating)
TpRouter.instance.removeRoute(SomeRoute());

// Remove routes matching condition
TpRouter.instance.removeWhere((data) => data.fullPath.contains('/temp'));

📦 Parameters #

TpRouter provides powerful, type-safe parameter parsing.

Path Parameters #

Use @Path() to extract values from the URL path.

@TpRoute(path: '/user/:id')
class UserPage extends StatelessWidget {
  const UserPage({required this.userId});
  
  @Path('id')
  final String userId;
}

// Navigation
UserRoute(userId: '123').tp(); // -> /user/123

Query Parameters #

Use @Query() to extract values from the query string.

@TpRoute(path: '/search')
class SearchPage extends StatelessWidget {
  const SearchPage({this.query, this.page});
  
  @Query('q')
  final String? query;
  
  @Query('page')
  final int? page; // Auto-parsed to int
}

// Navigation
SearchRoute(query: 'flutter', page: 2).tp(); // -> /search?q=flutter&page=2

Extra (Complex Objects) #

For non-serializable objects (like models), they are passed via memory.

@TpRoute(path: '/profile')
class ProfilePage extends StatelessWidget {
  const ProfilePage({required this.user});
  final User user; // Complex object, not in URL
}

// Navigation
ProfileRoute(user: currentUser).tp();

⚠️ Note: Extra objects are NOT preserved during:

  • Browser Refresh
  • Direct URL entry (e.g. typing URL in address bar)
  • App kill/restart

For persistent data, use path/query params or state management services.

Combined Example #

@TpRoute(path: '/order/:orderId/detail')
class OrderDetailPage extends StatelessWidget {
  const OrderDetailPage({
    required this.orderId,
    this.highlightItem,
    required this.orderData,
  });
  
  @Path('orderId')
  final String orderId;
  
  @Query('highlight')
  final String? highlightItem;
  
  final Order orderData; // Extra (passed via memory)
}

// Navigation
OrderDetailRoute(
  orderId: 'ORD-123',
  highlightItem: 'item-5',
  orderData: order,
).tp();
// URL: /order/ORD-123/detail?highlight=item-5
// orderData passed via memory

🛡️ Guards & Redirects #

Route-Level Redirect #

Protect specific routes. The redirect parameter accepts a TpRedirect<T> class.

// 1. Define the guard
class AuthGuard extends TpRedirect<ProtectedRoute> {
  @override
  FutureOr<TpRouteData?> handle(BuildContext context, ProtectedRoute route) {
    // Access the typed route object!
    if (!AuthService.instance.isLoggedIn) {
      return const LoginRoute(); // Redirect to login
    }
    return null; // Proceed (allow access)
  }
}

// 2. Apply to route
@TpRoute(path: '/protected', redirect: AuthGuard)
class ProtectedPage extends StatelessWidget { ... }

Global Redirect #

For app-wide rules (e.g., onboarding check, maintenance mode).

final router = TpRouter(
  routes: tpRoutes,
  redirect: (context, state) {
    // state.fullPath is the target URL
    if (needsOnboarding && state.fullPath != '/onboarding') {
      return OnboardingRoute();
    }
    if (isLoggedIn && state.fullPath == '/login') {
      return HomeRoute(); // Already logged in, skip login
    }
    return null; // Allow
  },
);

Reactive Routing (refreshListenable) #

This is how you make guards respond to state changes (e.g., login/logout).

Without refreshListenable, the router doesn't know when to re-evaluate guards. After login, you'd be stuck on the login page even if the guard logic allows access.

// 1. Create a listenable auth service
class AuthService extends ChangeNotifier {
  static final instance = AuthService();
  
  bool _isLoggedIn = false;
  bool get isLoggedIn => _isLoggedIn;

  void login() {
    _isLoggedIn = true;
    notifyListeners(); // 🔔 Signal the router!
  }

  void logout() {
    _isLoggedIn = false;
    notifyListeners(); // 🔔 Signal the router!
  }
}

// 2. Pass to TpRouter
final router = TpRouter(
  routes: tpRoutes,
  refreshListenable: AuthService.instance, // <-- KEY!
  redirect: (context, state) {
    final loggedIn = AuthService.instance.isLoggedIn;
    final isOnLogin = state.fullPath == '/login';
    
    if (!loggedIn && !isOnLogin) return LoginRoute();
    if (loggedIn && isOnLogin) return HomeRoute();
    return null;
  },
);

// 3. Now when you call login()...
AuthService.instance.login();
// ...the router automatically re-evaluates and redirects!

How it works:

  1. User navigates to /protected.
  2. Guard runs, isLoggedIn is false → redirect to /login.
  3. User logs in → AuthService.login() calls notifyListeners().
  4. TpRouter (listening to refreshListenable) re-runs the redirect logic.
  5. Now isLoggedIn is true → user is allowed through (or redirected to home if on login page).

🔄 Route Lifecycle #

OnExit Guard #

Intercept back navigation (e.g., unsaved changes confirmation).

// 1. Define the exit guard
class UnsavedChangesGuard extends TpOnExit<EditorRoute> {
  @override
  FutureOr<bool> onExit(BuildContext context, EditorRoute route) async {
    final shouldExit = await showDialog<bool>(
      context: context,
      builder: (c) => AlertDialog(
        title: Text('Unsaved Changes'),
        content: Text('Discard changes?'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(c, false), child: Text('Cancel')),
          TextButton(onPressed: () => Navigator.pop(c, true), child: Text('Discard')),
        ],
      ),
    );
    return shouldExit ?? false; // true = allow exit, false = block
  }
}

// 2. Apply to route
@TpRoute(path: '/edit', onExit: UnsavedChangesGuard)
class EditorPage extends StatelessWidget { ... }

🔗 Deep Linking #

TpRouter fully supports deep linking out of the box. All path and query parameters are automatically parsed.

How it works:

  1. Define your route with path parameters: @TpRoute(path: '/product/:id').
  2. When a deep link like yourapp://product/123?ref=email is opened:
    • id is extracted as '123'.
    • ref is extracted as 'email'.
  3. Your page receives fully typed parameters.

Example:

@TpRoute(path: '/product/:productId')
class ProductPage extends StatelessWidget {
  const ProductPage({required this.productId, this.referrer});
  
  @Path('productId')
  final String productId;
  
  @Query('ref')
  final String? referrer;
}

Deep link URL: https://example.com/product/abc123?ref=instagram

Platform Setup:

  • iOS: Configure Associated Domains in Xcode.
  • Android: Add intent-filter to AndroidManifest.xml.
  • Web: Works automatically.

See go_router deep linking guide for platform-specific setup (TpRouter uses go_router internally).


🎨 Page Transitions #

Built-in Transitions #

@TpRoute(
  path: '/details',
  transition: TpSlideTransition(), // TpSlideTransition, TpFadeTransition, etc.
  transitionDuration: 300,        // milliseconds
  reverseTransitionDuration: 200, // milliseconds (optional)
)
class DetailsPage extends StatelessWidget { ... }

Available transitions:

Transition Description
TpSlideTransition Slide from right
TpFadeTransition Fade in/out
TpScaleTransition Scale up/down
TpNoTransition No animation
TpCupertinoPageTransition iOS-style slide

Custom Transitions #

Implement TpTransitionsBuilder:

class MyCustomTransition extends TpTransitionsBuilder {
  const MyCustomTransition();
  
  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return RotationTransition(
      turns: animation,
      child: child,
    );
  }
}

// Apply globally
final router = TpRouter(
  routes: tpRoutes,
  defaultTransition: MyCustomTransition(),
);

// Or per-route (via annotation - requires custom setup)

Swipe Back #

Enable full-screen swipe-to-go-back gesture.

// Global default
final router = TpRouter(
  routes: tpRoutes,
  defaultPageType: TpPageType.swipeBack,
);

// Or use Cupertino-style (edge swipe only)
defaultPageType: TpPageType.cupertino,

📄 Page Configuration #

The @TpRoute annotation supports rich page configuration options for dialogs, modals, transparency, and more.

Page Type (TpPageType) #

Control how the page is rendered:

@TpRoute(
  path: '/settings',
  type: TpPageType.cupertino, // Force iOS-style page
)
class SettingsPage extends StatelessWidget { ... }
Type Description
TpPageType.auto Platform-adaptive (default). Material on Android, Cupertino on iOS.
TpPageType.material Force MaterialPage (Android-style).
TpPageType.cupertino Force CupertinoPage (iOS-style with edge swipe).
TpPageType.swipeBack Full-screen swipe-to-dismiss gesture.
TpPageType.custom Use with pageBuilder for fully custom pages.

Note on TpPageType.auto:

  • Android: Uses ZoomPageTransitionsBuilder (Android 10+) or standard slide up/fade.
  • iOS: Uses CupertinoPageTransition (slide from right with swipe-back).
  • macOS/Linux/Windows: Uses ZoomPageTransitionsBuilder or standard fade.

This ensures your app feels native on every platform without manual configuration.

Dialog & Modal Options #

Create fullscreen dialogs (iOS modal sheets):

@TpRoute(
  path: '/create-post',
  fullscreenDialog: true, // Shows close button instead of back arrow on iOS
)
class CreatePostPage extends StatelessWidget { ... }

Transparent Pages #

Create transparent overlays, bottom sheets, or custom modals:

@TpRoute(
  path: '/overlay',
  opaque: false,                          // Page is transparent
  barrierColor: Color(0x80000000),         // Semi-transparent black barrier
  barrierDismissible: true,                // Tap barrier to close
  barrierLabel: 'Dismiss overlay',         // Accessibility label
)
class OverlayPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        height: 300,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
        ),
        child: Text('Bottom Sheet Content'),
      ),
    );
  }
}

Full @TpRoute Reference #

All available @TpRoute parameters:

@TpRoute(
  // === Core ===
  path: '/user/:id',              // URL path pattern
  isInitial: false,               // Mark as initial route
  parentNavigatorKey: SomeNavKey, // Nest under a shell
  
  // === Guards ===
  redirect: AuthGuard,            // Redirect logic (TpRedirect<T>)
  onExit: UnsavedChangesGuard,    // Exit interception (TpOnExit<T>)
  
  // === Page Type ===
  type: TpPageType.auto,          // auto, material, cupertino, swipeBack, custom
  pageBuilder: MyCustomPage,      // Custom Page factory (overrides type)
  
  // === Transitions ===
  transition: TpSlideTransition(),        // Custom transition builder
  transitionDuration: Duration(milliseconds: 300),
  reverseTransitionDuration: Duration(milliseconds: 300),
  
  // === Dialog/Modal ===
  fullscreenDialog: false,        // iOS modal style (close button)
  opaque: true,                   // false = transparent page
  barrierDismissible: false,      // Tap outside to dismiss
  barrierColor: null,             // Barrier color (e.g. Color(0x80000000))
  barrierLabel: null,             // Accessibility label for barrier
  
  // === State ===
  maintainState: true,            // Keep state when inactive
)
class MyPage extends StatelessWidget { ... }

Full @TpShellRoute Reference #

@TpShellRoute supports page configuration plus shell-specific options:

@TpShellRoute(
  // === Core ===
  navigatorKey: MainNavKey,           // Required: Shell identifier
  parentNavigatorKey: RootNavKey,     // Optional: Nest shells
  isIndexedStack: true,               // Use StatefulShellRoute (preserves tab state)
  branchKeys: [HomeNavKey, ProfileNavKey], // Branch identifiers for IndexedStack
  
  // === Observers ===
  observers: [MyNavigatorObserver, AnalyticsObserver], // NavigatorObservers for this shell
  
  // === Page Configuration (same as TpRoute) ===
  type: TpPageType.material,
  fullscreenDialog: false,
  opaque: true,
  barrierDismissible: false,
  barrierColor: null,
  barrierLabel: null,
  maintainState: true,
  pageBuilder: MyShellPageBuilder,    // Custom Page factory
)
class MainShell extends StatelessWidget { ... }

Observer Example:

class AnalyticsObserver extends NavigatorObserver {
  @override
  void didPush(Route route, Route? previousRoute) {
    analytics.logPageView(route.settings.name);
  }
}

@TpShellRoute(
  navigatorKey: MainNavKey,
  observers: [AnalyticsObserver], // Attach to shell's navigator
)
class MainShell extends StatelessWidget { ... }

⚙️ Configuration #

TpRouter Options #

TpRouter(
  routes: tpRoutes,
  
  // Initial location (auto-detected from isInitial if not set)
  initialLocation: '/home',
  
  // Global redirect
  redirect: (context, state) => null,
  
  // Reactive routing trigger
  refreshListenable: authNotifier,
  
  // Error page
  errorBuilder: (context, state) => ErrorPage(error: state.error),
  
  // Debug logging
  debugLogDiagnostics: true,
  
  // Transition defaults
  defaultTransition: TpSlideTransition(),
  defaultTransitionDuration: Duration(milliseconds: 300),
  defaultReverseTransitionDuration: Duration(milliseconds: 200),
  
  // Page type: auto, material, cupertino, swipeBack
  defaultPageType: TpPageType.auto,
  
  // Custom navigator key (must be a specific key instance)
  navigatorKey: const RootNavKey(),
  
  // Restoration for state persistence
  restorationScopeId: 'app_router',
  
  // Redirect limit to prevent infinite loops
  redirectLimit: 5,
);

build.yaml Options #

Customize generator output:

targets:
  $default:
    builders:
      tp_router_generator:
        options:
          output: lib/routes/app_routes.dart # Custom output path

📝 License #

MIT License. See LICENSE for details.

2
likes
160
points
337
downloads

Publisher

verified publisherpub.lwjlol.com

Weekly Downloads

A simplified Flutter router based on go_router with annotation support.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, go_router, tp_router_annotation

More

Packages that depend on tp_router