Tutorial
Navigating Between Pages
pushNamed and pop first, then results, named locations, replacing and resetting the stack.
The two you already used
-
pushNamed(location)adds a page. The argument is a location (a path), not a route name. -
pop([result])removes the top page immediately.maybePop()asks first (the page'sPopScope, then the route'sonExit) — use it for back buttons.
There is no push(Route): every page must be in the route table so the URL can always describe the stack.
Three more verbs, one at a time
| Verb | In this step |
|---|---|
await pushNamed<int>(...)
+
pop(stars)
|
The detail page opens
/products/:id/reviews
and waits; the reviews page calls
pop(4)
; the
Future
completes with
4
.
|
namedLocation(name, pathParameters:) |
Builds
/products/2
from the route named
product
, so paths are not glued together by hand.
|
pushReplacementNamed(location) |
“Next product” swaps the top page. Back still goes to the list, not to the previous product. |
pushNamedAndRemoveUntil(location, (_) => false) |
“Back to the list” rebuilds the whole stack from a URL — the same call you will use after sign-in and for deep links. |
The complete file
The route :id gets a name and a child
reviews; the detail page gets three buttons; a ReviewsPage
is added.
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',
name: 'product', // lets you build locations with namedLocation
builder: (context, state) => ProductDetailPage(id: state.pathParameters['id']),
routes: [
AdaptiveRoute(
path: 'reviews', // /products/:id/reviews
builder: (context, state) => const ReviewsPage(),
),
],
),
],
),
],
);
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 StatefulWidget {
const ProductDetailPage({super.key, required this.id});
final String? id;
@override
State<ProductDetailPage> createState() => _ProductDetailPageState();
}
class _ProductDetailPageState extends State<ProductDetailPage> {
int? _stars;
@override
Widget build(BuildContext context) {
final product = findProduct(widget.id);
if (product == null) {
return Scaffold(
appBar: AppBar(),
body: Center(child: Text('No product ${widget.id}')),
);
}
final router = AdaptiveRouter.of(context);
final next = products[product.id % products.length]; // wraps around
return Scaffold(
appBar: AppBar(
leading: BackButton(onPressed: () => router.maybePop()),
title: Text(product.name),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text('\$${product.price}', style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 16),
// pushNamed returns a Future that completes with the value passed to pop.
FilledButton(
onPressed: () async {
final stars = await router.pushNamed<int>('/products/${product.id}/reviews');
if (stars != null && mounted) setState(() => _stars = stars);
},
child: Text(_stars == null ? 'Rate this product' : 'Your rating: $_stars stars'),
),
// pushReplacementNamed swaps the top page: back still returns to the list.
OutlinedButton(
onPressed: () => router.pushReplacementNamed(
router.namedLocation('product', pathParameters: {'id': '${next.id}'}),
),
child: Text('Next: ${next.name}'),
),
// pushNamedAndRemoveUntil with (_) => false rebuilds the whole stack from a URL.
TextButton(
onPressed: () => router.pushNamedAndRemoveUntil('/products', (_) => false),
child: const Text('Back to the list'),
),
],
),
);
}
}
class ReviewsPage extends StatelessWidget {
const ReviewsPage({super.key});
@override
Widget build(BuildContext context) {
final router = AdaptiveRouter.of(context);
return Scaffold(
appBar: AppBar(
leading: BackButton(onPressed: () => router.maybePop()),
title: const Text('Rate'),
),
body: ListView(
children: [
for (var stars = 5; stars >= 1; stars--)
ListTile(
title: Text('$stars stars'),
// pop(result) completes the pushNamed Future on the previous page.
onTap: () => router.pop(stars),
),
],
),
);
}
}
What you will see
- Detail → Rate this product → pick “4 stars”: you are back on the detail and the button reads “Your rating: 4 stars”.
-
Next: Notebook replaces the page: the URL changes to
/#/products/2, and back returns to the list. -
Back to the list clears everything above
/products.
Next: Adding tabs.