Tutorial
Nested Routes & Parameters
Add a product detail page at /products/:id and read path and query parameters.
Three path rules
-
Child paths are relative to their parent: a child
:idunder/productsmatches/products/1. -
`:param` matches exactly one path segment. Its value arrives in
state.pathParameters. - First match wins, in the order you declare routes.
Matching /products/2 walks the tree and collects every route on the way:
[products, 2]. That list is the page stack. So a deep link to a detail page still has the list beneath it, and back goes to the list.
The complete file
Compared to the previous step: a small Product model, a child route
:id, a list that calls pushNamed, and a detail page.
import 'package:material_ui/material_ui.dart';
import 'package:xue_hua_adaptive_sliding_layout/xue_hua_adaptive_sliding_layout.dart';
class Product {
const Product(this.id, this.name, this.price);
final int id;
final String name;
final double price;
}
const products = [
Product(1, 'Coffee mug', 12),
Product(2, 'Notebook', 8),
Product(3, 'Desk lamp', 39),
];
Product? findProduct(String? id) {
final value = int.tryParse(id ?? '');
for (final product in products) {
if (product.id == value) return product;
}
return null;
}
final router = AdaptiveRouter(
initialLocation: '/products',
routes: [
AdaptiveRoute(
path: '/products',
builder: (context, state) => const ProductsPage(),
routes: [
// Child paths are relative: this matches /products/1, /products/2 ...
AdaptiveRoute(
path: ':id',
builder: (context, state) => ProductDetailPage(id: state.pathParameters['id']),
),
],
),
],
);
void main() => runApp(const ShopApp());
class ShopApp extends StatelessWidget {
const ShopApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(title: 'Shop', routerConfig: router);
}
}
class ProductsPage extends StatelessWidget {
const ProductsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: ListView(
children: [
for (final product in products)
ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
onTap: () => AdaptiveRouter.of(context).pushNamed('/products/${product.id}'),
),
],
),
);
}
}
class ProductDetailPage extends StatelessWidget {
const ProductDetailPage({super.key, required this.id});
final String? id;
@override
Widget build(BuildContext context) {
final product = findProduct(id);
// The query string is available too: /products/1?ref=home
final ref = AdaptiveRouteState.of(context).queryParameters['ref'];
return Scaffold(
appBar: AppBar(
leading: BackButton(onPressed: () => AdaptiveRouter.of(context).maybePop()),
title: Text(product?.name ?? 'Not found'),
),
body: Center(
child: Text(
product == null ? 'No product $id' : '\$${product.price} (ref: ${ref ?? '-'})',
),
),
);
}
}
What you will see
- The list shows three products. Tapping one pushes its detail page; the AppBar back arrow returns to the list.
-
Open
/#/products/2?ref=maildirectly in the browser: the detail opens withref: mail, and back still leads to the list, because the URL produced the stack[products, 2]. -
Open
/#/products/99: the route matches,findProductreturns null and the page says so. (URLs that match no route go to the router'serrorBuilder, covered in Redirects.)
Where parameters come from
The builder receives an AdaptiveRouteState; inside the page you can also call
AdaptiveRouteState.of(context). Useful fields: pathParameters,
queryParameters, uri, arguments,
name.
Next: Navigating between pages.