ffca-routing · git:20260929.ceab714 · 2026-09-29 · sha256 5b82ba25729d4c08
ffca-routing git:20260929.ceab714A
Immutable. This exact content never changes and is served at /api/v1/blob/5b82ba25729d4c08.
---
name: ffca-routing
description: >
Routing and navigation for FFCA monorepos: callback injection, go_router_builder typed routes,
splitting the routing table, deferred-loaded features, the $extra hydration pattern, and keeping
features isolated from each other's routes. Use in an FFCA monorepo, a features/ folder whose
packages are named {feature}_domain, {feature}_data, and {feature}_presentation, when adding
screens, routes, navigation, deep links, navigation callbacks, or a deferred-loaded feature.
Triggers on "add a route for this screen", "navigate from one feature to another", "where do
routes live", "split the router", "lazy load this feature", "pass an object to the next screen",
"hydrate $extra on deep link", "add a deep link", "wire a navigation callback", and "can this
feature import the router". If the repo instead uses packages/ with _repository and _api_client
packages, defer to vgv-ai-flutter-plugin's layered-architecture skill.
allowed-tools: Read Glob Grep Write Edit
effort: high
---
# FFCA Routing
Workflow for wiring navigation in an FFCA monorepo. The patterns and code shapes live in `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md` and in `${CLAUDE_PLUGIN_ROOT}/references/code_templates/presentation_templates.md`. Read them before wiring routes.
## The core constraint
Features are isolated, so a feature cannot know the app's global routing table. It never imports the app's router or another feature's routes. Navigation is a dependency the app layer injects: the app decides *what happens next*, the feature only detects *when* it is needed. Read the intro of `references/ffca/navigation.md`, then follow the pattern that fits the feature's depth.
## Choose the injection pattern
1. **Shallow widget trees: callbacks on the module.** The module constructor takes typed callbacks (`onProductTapped`, `onCheckoutStarted`) and the app supplies them. Read `references/ffca/navigation.md`, section *Simple features: pass callbacks directly*, and adapt the module example in `references/code_templates/presentation_templates.md`.
2. **Deep widget trees: a navigation interface.** Define a plain Dart class in the feature's presentation package holding every navigation action, require it in the module, and provide it to the subtree with `Provider.value`. Deep widgets then call `context.read<CartNavigation>().onProductTapped(id)` with no prop drilling. Read `references/ffca/navigation.md`, section *Deep trees: define a navigation interface*.
Whichever you pick, the callback at the module boundary is the compile-time-safe cross-feature contract.
## Wire routes in the app layer with go_router_builder
Use `go_router` with `go_router_builder`. The app defines `@TypedGoRoute` and `GoRouteData` classes that instantiate feature modules and wire their callbacks to typed route navigation. Adapt the `GoRouteData` template in `references/code_templates/presentation_templates.md`.
Two hard rules from `references/ffca/navigation.md`, section *Recommended: go_router with go_router_builder*:
- No string-based paths. Navigate with generated route classes, `ProductDetailRoute(id: id).go(context)`, never `context.push('/product/123')`. A string path fails at runtime instead of compile time, and it forces the feature to know the app's URL structure.
- Features never import the app router configuration or another feature's route classes.
## Split the routing table, but keep it one library
The app owns the routing table, so every feature lands in the same file and it becomes a merge-conflict magnet. Give each feature's routes their own file, with one constraint that is easy to get wrong.
`go_router_builder` collects every `@TypedGoRoute` it finds in a *library* into a single generated `$appRoutes`. Routes declared in a separate library are left out, **and the failure is quiet**: the build succeeds and the routes simply do not exist. So the per-feature files must be `part` of one library, not separate imports.
```text
apps/my_app/lib/app_router/
routes.dart the library: imports, part directives, root route
favorites_routes.dart part
ideas_routes.dart part
routes.g.dart generated part
```
`routes.dart` holds every import and every `part` directive; each feature file starts with `part of 'routes.dart';`. Adding a feature is two lines in the shared file plus a new file nobody else is editing. Read `references/ffca/navigation.md`, section *Splitting the routing table across files*.
## Deferred imports live in the routing table
Because all imports are centralized in `routes.dart`, that is also where each feature gets its `deferred as` prefix, so its code is fetched the first time a route needs it rather than shipping in the initial bundle. Import a subfeature barrel rather than the whole package where one exists, so opening a list screen does not also download the detail screen.
```dart
import 'package:favorites_presentation/favorites_list.dart'
deferred as favorites_list;
```
Before relying on this, read `references/ffca/project_structure.md`, section *Deferred loading*. A deferred package must not also be reachable from the app through a non-deferred path, or the chunk never splits, silently. This is informational on an iOS/Android-only app, where the AOT snapshot contains the whole program regardless.
## Deep links and the $extra pattern
Every screen must be rebuildable from its URL parameters alone. `$extra` is a volatile optimization that is lost on browser refresh, process death, and cold-start deep links, so the route declares it nullable and the Bloc treats it as optional: emit `Loaded` immediately when it is present, otherwise fetch by id and show loading. Read `references/ffca/navigation.md`, section *Deep links and data hydration*, and adapt the `ProductDetailRoute` template.
## Visual extension points
The same inversion applies to widgets. A feature can declare a slot for a widget it does not own and let the app fill it, exactly as it declares a callback for a destination it does not own. Read `references/ffca/navigation.md`, section *Visual extension points*, then use the `ffca-cross-feature` skill to choose between a slot and a direct import.
## Add-to-app
A module is a self-contained entry point with explicit dependencies, so it can be instantiated directly as the root widget of a `FlutterEngine` instead of inside a `GoRouteData.build()`. The module code is identical; only the callback targets change, from go_router routes to platform channels. Read `references/ffca/project_structure.md`, section *Add-to-app support*.