simple_routes 1.0.0-beta.1 simple_routes: ^1.0.0-beta.1 copied to clipboard
Simple, type-safe route and navigation management for go_router.
Simple Routes #
Simple, type-safe route and navigation management for go_router.
Stable release #
We have reached a stable release of Simple Routes with v1.0.0-beta.1! 🎉
This release includes several breaking changes from the 0.x.x versions.
Please see the Migration Guide for more info.
Features #
Simple Routes is a companion package to GoRouter that provides a simple, type-safe way to define your app's routes and navigate between them.
- Eliminate "magic strings" and the bugs that come with them
- Simplify route definitions and navigation invocation
- Enforce type-safe routing requirements
- Inject and extract path parameters, query parameters, and "extra" route data
Table of Contents #
Getting started #
This package is intended to be used with the GoRouter package.
dependencies:
go_router: ^12.0.0
simple_routes: ^1.0.0-beta.1
Usage #
Route definitions #
Basic (simple) routes
Define your routes as classes that extend the SimpleRoute
base class.
class ProfileRoute extends SimpleRoute {
const ProfileRoute();
@override
String get path => 'profile';
}
Override the path
property with the route's path segment.
If your route is not a child of another route (more on this below), the path will automatically be prefixed with a leading slash, when appropriate.
Route parameters and DataRoutes
For routes that require parameters, extend the DataRoute
class.
// Some class or object that you want to pass with your route.
class MyExtraData {
const MyExtraData(this.someValue);
final String someValue;
}
// Define your route and/or query parameters as an enum
enum RouteParams {
userId,
query,
}
// Define a data class that extends SimpleRouteData
//
// This class should carry all of the data that your routing
// requires, including path parameters, query parameters, and
// "extra" data that you want to pass to your route.
class UserRouteData extends SimpleRouteData {
const UserRouteData({
required this.userId,
required this.extraData,
this.queryValue,
});
// Use a factory constructor to simplify extracting data from
// the GoRouterState.
factory UserRouteData.fromState(GoRouterState state) {
// Use the extension methods to simplify extracting data from
// the GoRouterState by providing the enum value or data type.
//
// It is recommended to use these same extensions to validate the
// presence of the required data in a `redirect` - more on this in
// the GoRouter configuration section below.
final userId = state.getParam(RouteParams.userId)!;
final queryValue = state.getQuery(RouteParams.query);
final extraData = state.getExtra<MyExtraData>()!;
return UserRouteData(
userId: userId,
queryValue: queryValue,
extraData: extraData,
);
}
// For example, a "user ID" parameter for the path
// i.e. /user/:userId
final String userId;
// Or a query parameter
final String? queryValue;
// Or any other data that you want discretely passed to your route.
final MyExtraData extraData;
// Override the `parameters` property with a map of your
// route's path parameters. These will be automatically injected
// into the route path.
@override
Map<Enum, String> get parameters => {
RouteParams.userId: userId,
};
// Override the `query` property with a map of your route's
// query parameters. These will be automatically URL encoded
// and appended to the end of your path.
//
// The query map allows null values, so you don't have to worry
// about whether or not to include a query parameter.
@override
Map<Enum, String?> get query => {
RouteParams.query: queryValue,
};
// Override the `extra` property with any extra data that you
// want passed along with your route.
@override
MyExtraData get extra => extraData;
}
// Define the route as a DataRoute, typed for your data class.
class UserRoute extends DataRoute<UserRouteData> {
const UserRoute();
// Define the route path using the appropriate enum value.
// Use the `prefixed` property to automatically prefix the
// enum value name with a colon (e.g. ":userId").
//
// To define a path with multiple segments, create an
// `Iterable<String>` and use the `toPath` extension method.
@override
String get path => ['user', RouteParams.userId.prefixed].toPath();
}
Child routes #
To define a route that is a child of another route, implement the ChildRoute
interface, providing the parent route type and overriding the parent
property.
class UserDetailsRoute extends DataRoute<UserRouteData> implements ChildRoute<UserRoute> {
const UserDetailsRoute();
// Define the route path segment. No need to worry about
// leading slashes - they will be added automatically.
@override
String get path => 'details';
// Define the parent route. This will be used to
// construct the full path for this route.
@override
UserRoute get parent => const UserRoute();
}
Note: Routes that are children of a DataRoute
must also be a DataRoute
themselves, even if they don't require any data. In cases like these, you can re-use the parent's data class and factory constructor.
However, if they require their own data, the data class must provide it and the data necessary for the parent(s).
GoRouter configuration #
Configuring your GoRoute
s is easy. Create an instance of your class and pass in the goPath
property to your route's path
parameter.
GoRouter(
// Note that the initialLocation should use the "fullPath" property
// to include any parent routes, if applicable.
initialLocation: const HomeRoute().fullPath,
routes: [
GoRoute(
path: const HomeRoute().goPath,
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: const UserRoute().goPath,
redirect: (context, state) {
// Use the extension methods to validate that any and all
// required values are present.
if (state.getParam(RouteParams.userId) == null) {
// If the data is not present, redirect to another route
// using the `fullPath` property - this is important, as
// the `path` and `goPath` properties only include the
// route's segment(s), but not the fully-qualified path.
return const HomeRoute().fullPath;
}
// If all of the data is present, return null to allow the
// route to be built.
return null;
},
builder: (context, state) {
final routeData = UserRouteData.fromState(state);
return UserScreen(
userId: routeData.userId,
query: routeData.queryValue,
extra: routeData.extraData,
);
},
routes: [
// Define the child route, using the same data class as
// the parent route.
GoRoute(
path: const UserDetailsRoute().goPath,
builder: (context, state) {
final routeData = UserRouteData.fromState(state);
return UserDetailsScreen(
userId: routeData.userId,
);
},
),
],
),
],
);
DataRoute generation
If you need to redirect to a DataRoute, or otherwise need the complete path for a DataRoute, you must use the generate
method to generate the full path. The fullPath
property will include the template values and will not route properly.
For example, given the following route:
class MyRoute extends DataRoute<MyRouteData> {
const MyRoute();
@override
String get path => ['user', RouteParams.userId.prefixed].toPath();
}
...
// This will not work!
// The return value will be `/user/:userId`
redirect: (context, state) {
return const MyRoute().fullPath;
}
...
// Instead, use `generate`, like so:
redirect: (context, state) {
return const MyRoute().generate(MyRouteData(userId: '123'));
}
Navigation #
Once your routes are defined and your router is configured, you can navigate between your routes using the go
and push
methods.
onPressed: () => const HomeRoute().go(context),
For your routes that require parameters, the go
method will enforce that you pass an instance of your data class.
onPressed: () => const UserRoute().go(
context,
data: UserRouteData(
userId: '123',
queryValue: 'some query value',
extraData: MyExtraData('some extra data'),
),
),
Note: The push
method signatures are identical to their corresponding SimpleRoute/DataRoute go
methods.
Advanced usage #
Route matching #
Current route
The isCurrentRoute
method will determine if your app is at a particular route.
For example, given the following routes:
class BaseRoute extends SimpleRoute {
const BaseRoute();
@override
String get path => 'base';
}
class SubRoute extends SimpleRoute implements ChildRoute<BaseRoute> {
const SubRoute();
@override
String get path => 'sub';
@override
BaseRoute get parent => const BaseRoute();
}
and your app is at the location of /base/sub
:
// current location: '/base/sub'
if (const SubRoute().isCurrentRoute(context)) {
debugPrint('We are at SubRoute!');
}
Your app will print We are at SubRoute!
.
Parent route
Similar to isCurrentRoute
, you can use the isParentRoute
method to check whether a route is a parent of the current location.
For example, if your app is at the location of /base/sub
:
// current location: '/base/sub'
if (const BaseRoute().isParentRoute(context)) {
debugPrint('We are at a child of BaseRoute!');
}
Your app will print We are at a child of BaseRoute!
.
Note: this method will return false
if the current route is an exact match for the route in question (i.e. isCurrentRoute
).
For example, if we are at the /base/sub
location and use isParentRoute
, it will return false
:
// current location: '/base/sub'
if (const SubRoute().isParentRoute(context)) {
debugPrint('We are at a child of SubRoute!');
}
In this case, the print statement will not be executed.