navhost_typed 0.1.0
navhost_typed: ^0.1.0 copied to clipboard
Type-safe destination objects and navigation helpers for navhost.
navhost_typed #
Type-safe destination objects for navhost.
navhost_typed keeps navhost lightweight while removing stringly-typed navigation from app code. Define destinations as small Dart objects, navigate with those objects, and keep URL parsing only at the deep-link boundary.
Why #
With plain navhost, navigation usually starts as strings:
context.navController.navigate('/posts/42?from=feed');
That is simple, but large apps quickly accumulate duplicated path builders, query keys, and route parsing. navhost_typed keeps the same navhost runtime and adds a thin typed layer:
context.navigateTyped(PostDestination(postId: '42', from: 'feed'));
Core idea #
A destination is a navigation intent. Declare template, override pathParams and queryParams — the concrete location is computed automatically. Add a result type T when the destination can return a value.
// No result
class PostDestination extends TypedDestination {
static const pathTemplate = '/posts/:postId';
final String postId;
final String? from;
const PostDestination({required this.postId, this.from});
@override String get template => pathTemplate;
@override Map<String, String> get pathParams => {'postId': postId};
@override Map<String, String?> get queryParams => {'from': from};
@override
Widget build(BuildContext context) => PostPage(postId: postId, from: from);
static PostDestination fromRoute(
Map<String, String> params,
Map<String, String> query,
) =>
PostDestination(postId: params['postId'] ?? '', from: query['from']);
}
// With result — T is inferred at call sites
class ConfirmDestination extends TypedDestination<bool> {
const ConfirmDestination({required this.title});
final String title;
@override String get template => '/confirm';
@override Map<String, String?> get queryParams => {'title': title};
@override Widget build(BuildContext context) => ConfirmDialog(title: title);
static ConfirmDestination fromRoute(_, Map<String, String> q) =>
ConfirmDestination(title: q['title'] ?? 'Confirm?');
}
Register destinations as normal navhost routes:
final navController = NavController(
routes: [
typedRoute(PostDestination.pathTemplate, PostDestination.fromRoute),
typedRoute(ConfirmDestination.pathTemplate, ConfirmDestination.fromRoute),
],
);
Installation #
dependencies:
navhost: ^latest
navhost_typed: ^latest
import 'package:navhost/navhost.dart';
import 'package:navhost_typed/navhost_typed.dart';
API #
TypedDestination<T> #
abstract class TypedDestination<T extends Object?> {
const TypedDestination();
String get template;
Map<String, String> get pathParams => const {};
Map<String, String?> get queryParams => const {};
// Derived from template + pathParams + queryParams. Override only for
// non-standard URLs (fragments, custom encoding, etc.).
String get location => buildLocation(pathParams, query: queryParams);
Widget build(BuildContext context);
}
| Getter | Purpose | Default |
|---|---|---|
template |
Route pattern, e.g. /posts/:postId |
— (abstract) |
pathParams |
Values for each :param segment |
{} |
queryParams |
Query string entries; null values are omitted |
{} |
location |
Concrete URL used for navigation | derived |
T |
Result type returned when this destination is popped | Object? |
Omit T for destinations that do not return a result.
buildLocation
TypedDestination exposes a buildLocation helper used internally to derive location. It can also be called when overriding location with custom logic:
@override
String get location => buildLocation({'id': id}, query: {'ref': ref}) + '#top';
TypedNavInterceptor #
A NavInterceptor subclass that works with typed destinations. Override interceptTyped and return a TypedDestination to redirect, or null to allow the navigation.
class AuthInterceptor extends TypedNavInterceptor {
@override
TypedDestination? interceptTyped(String from, String to) {
if (!isAuthenticated && Uri.parse(to).path.startsWith('/profile')) {
return AuthDestination(next: to); // no .location needed
}
return null;
}
}
typedRoute #
typedRoute(ProfileDestination.pathTemplate, ProfileDestination.fromRoute)
Converts incoming navhost path/query params into the typed destination and
delegates page creation to destination.build(context).
Typed navigation helpers #
On NavController
// T inferred from destination's result type
navController.navigateTyped(const HomeDestination(), launchSingleTop: true);
navController.navigateTyped(
ProfileDestination('mathias'),
popUpTo: const AuthDestination(),
popUpToInclusive: true,
);
navController.switchTyped(const HomeDestination());
// Future<int?> — T inferred from TypedDestination<int>
final count = await navController.showBottomSheetTyped(CommentsDestination(postId));
// Future<bool?> — T inferred from TypedDestination<bool>
final confirmed = await navController.showDialogTyped(const ConfirmDestination(title: 'Delete?'));
navController.pushTyped(SomeDestination());
navController.pushBottomSheetTyped(SomeDestination());
navController.pushDialogTyped(SomeDestination());
On BuildContext
context.navigateTyped(ProfileDestination('mathias'));
All helpers call regular navhost APIs under the hood, so interceptors,
popUpTo, launchSingleTop, results, bottom sheets, dialogs, and nested
NavHosts keep working.
Recommended conventions #
- Put each destination near its page, or in a
ui/destinationsfolder when routes are shared widely. - Prefer constructor fields for internal navigation.
- Keep parsing in
fromRoute; that is the only URL boundary. - Declare
extends TypedDestination<T>only when the destination actually returns a result. - Keep rich objects inside constructors for internal navigation, but encode only stable IDs in
locationfor deep links.
Example #
The example includes:
- path parameters
- query parameters
- typed auth redirect via
TypedNavInterceptor launchSingleTop- typed
popUpTo - bottom sheets with inferred result type
- dialogs with inferred result type
- awaiting typed results
License #
MIT