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 :id under /products matches /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.

dart lib/main.dart
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=mail directly in the browser: the detail opens with ref: mail, and back still leads to the list, because the URL produced the stack [products, 2].
  • Open /#/products/99: the route matches, findProduct returns null and the page says so. (URLs that match no route go to the router's errorBuilder, 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.