Руководства
Первый типизированный Route
Запускаемый сценарий Catalog и Product с каноническим URL, одним владельцем Session, точным Push и типизированным результатом.
Первый типизированный Route
Вы соберёте приложение из двух экранов. На Home есть кнопка Открыть товар. Product показывает типизированный productId и возвращает true после покупки. Home отображает результат. Прямое открытие /products/keyboard-42 создаёт оба экрана, поэтому Back ведёт на Home.
Для первого запуска оставьте весь код в lib/main.dart. Разделить Destination, Route и страницы можно после проверки сценария.
Установите и импортируйте пакет
flutter pub add ark_navigation:^1.1.0
import 'package:ark_navigation/ark_navigation.dart';
import 'package:flutter/material.dart';
Опишите типизированное намерение
final class HomeDestination extends NavigationDestination<void> {
const HomeDestination();
}
final class ProductDestination extends NavigationDestination<bool> {
const ProductDestination({required this.productId});
final String productId;
}
Generic-параметр bool означает, что Product может завершиться через Pop со значением bool?. Destination не содержит Flutter Page, URI и зависимости от Router.
Свяжите Destination с Page и URI
final homeRoute = NavigationRouteDefinition<HomeDestination, void>(
id: const NavigationRouteId('home'),
pageBuilder: (context, destination, entry) => const HomePage(),
uriCodec: CallbackNavigationUriCodec<HomeDestination>(
pattern: '/',
encoder: (destination) => Uri(path: '/'),
decoder: (match) => const HomeDestination(),
),
);
final productRoute = NavigationRouteDefinition<ProductDestination, bool>(
id: const NavigationRouteId('product'),
pageBuilder: (context, destination, entry) => ProductPage(
productId: destination.productId,
),
uriCodec: CallbackNavigationUriCodec<ProductDestination>(
pattern: '/products/:productId',
encoder: (destination) => Uri(
pathSegments: <String>['products', destination.productId],
),
decoder: (match) => ProductDestination(
productId: match.path('productId'),
),
),
stackPlanBuilder: (destination) => <NavigationTarget<Object?>>[
const NavigationTarget<Object?>(destination: HomeDestination()),
NavigationTarget<Object?>(destination: destination),
],
);
stackPlanBuilder используется при прямом открытии Product URI и атомарно восстанавливает Home → Product. Обычный push добавляет Product к существующему стеку.
Соберите проверяемый Graph
NavigationGraph createGraph() {
const rootScopeId = NavigationScopeId('root');
const catalogModuleId = NavigationModuleId('catalog');
return NavigationGraph(
rootScopeId: rootScopeId,
scopes: const <NavigationScopeDefinition>[
NavigationScopeDefinition(
id: rootScopeId,
restorationScopeId: 'root-navigation',
),
],
modules: <NavigationModule>[
NavigationModule(
id: catalogModuleId,
routes: <NavigationRouteDefinitionBase>[homeRoute, productRoute],
),
],
mounts: <NavigationMount>[
NavigationMount(
id: const NavigationMountId('catalog-root'),
moduleId: catalogModuleId,
scopeId: rootScopeId,
),
],
);
}
Module владеет Route Definition. Mount, принадлежащий приложению, помещает Module в корневой Scope и URI-префикс /.
Назначьте одного владельца Session
final class ExampleHost extends StatefulWidget {
const ExampleHost({super.key});
@override
State<ExampleHost> createState() => _ExampleHostState();
}
final class _ExampleHostState extends State<ExampleHost> {
late final NavigationSession session = NavigationSession(
id: const NavigationSessionId('main'),
graph: createGraph(),
);
@override
void dispose() {
session.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(routerConfig: session.routerConfig);
}
}
Не создавайте Session внутри build(): rebuild не должен заменять Controller, ключи Navigator, историю и ожидающие Ticket.
Откройте Product и проверьте результат операции
final NavigationTicket<bool> ticket = context.readNavigation.push<bool>(
const ProductDestination(productId: 'keyboard-42'),
);
final NavigationOperationResult commit = await ticket.committed;
if (commit is! NavigationCommitted) return;
switch (await ticket.completed) {
case NavigationPopped<bool>(:final result):
debugPrint('Purchased: $result');
case NavigationRemoved<bool>():
debugPrint('Entry удалён перестройкой стека.');
case NavigationNotCommitted<bool>():
throw StateError('Неуспешная операция уже обработана выше.');
}
После успешной фиксации корневой Scope содержит [Home, Product]. ticket.completed остаётся незавершённым до удаления Product.
Верните типизированный результат
FilledButton(
onPressed: () => context.readNavigation.pop(result: true),
child: const Text('Buy and return'),
)
Стек становится [Home], а вызывающий код получает NavigationPopped<bool>(result: true). Системный Back вернул бы тот же тип с null.
Добавьте две страницы
final class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
final class _HomePageState extends State<HomePage> {
String message = 'Результата пока нет';
Future<void> openProduct() async {
final ticket = context.readNavigation.push<bool>(
const ProductDestination(productId: 'keyboard-42'),
);
final commit = await ticket.committed;
if (commit is! NavigationCommitted) {
if (mounted) setState(() => message = 'Переход не выполнен');
return;
}
final completion = await ticket.completed;
if (!mounted) return;
setState(() {
message = switch (completion) {
NavigationPopped<bool>(:final result) => 'Покупка: $result',
NavigationRemoved<bool>() => 'Product удалён при перестройке стека',
NavigationNotCommitted<bool>() => 'Product не был открыт',
};
});
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Catalog')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(message),
FilledButton(onPressed: openProduct, child: const Text('Открыть товар')),
],
),
),
);
}
final class ProductPage extends StatelessWidget {
const ProductPage({required this.productId, super.key});
final String productId;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('Product $productId')),
body: Center(
child: FilledButton(
onPressed: () => context.readNavigation.pop(result: true),
child: const Text('Купить и вернуться'),
),
),
);
}
Запустите приложение. Если открылся Catalog, значит Session, Graph, Mount, Route Definition и Flutter Router соединены правильно. Кнопка проверяет типизированный переход и возврат результата.