NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Headless routing engine for ZenRouter family.
Last release 8 days ago
30 Sep 2026
Ships fairly regularly
a new release about every 6 weeks
Most releases are documented
notes for 6 of 7 stable releases
Nothing withdrawn
no release was ever pulled
7 months old
8 releases · first in 2026
One column per month.
chore: release ZenRouter 3.0.0 and prepare DartNative 0.1.0
chore: release ZenRouter 3.0.0 and prepare DartNative 0.1.0
Stable adapter-neutral routing engine. The changes below are cumulative since 2.3.0 and apply to both Flutter and DartNative adapters.
Route manifests now describe topology only. RouteManifestRoute no
longer duplicates query, guard, redirect, deferred-loading, or deep-link
metadata from concrete route implementations.
Navigation operations moved out of CoordinatorCore into composable
capability mixins. CoordinatorCore is now a state container only
(paths, URI parsing, layout-parent hooks).
| Mixin | Capabilities |
|---|---|
CoordinatorLayoutCore |
Layout-parent registration / hierarchy activation |
CoordinatorNavigatable |
navigate |
CoordinatorMutatable |
push, pushSilently, pop, replace, pushReplacement, pushOrMoveToTop, tryPop |
CoordinatorRecoverable |
recover, recoverUri, defineDeeplinkHandler |
Flutter Coordinator still composes the full capability set. Custom
CoordinatorCore subclasses must mix in the capabilities they need.
Route value hashing now follows Dart's equality contract. Mutable path
and result lifecycle state no longer contributes to hashCode;
RouteTarget.deepEquals now means same lifecycle entry (reference identity).
Equatable.internalProps is removed. Equality and hashing use
runtimeType + props only. Delete any leftover subclass overrides.
CoordinatorModular.defineModules now returns Iterable and snapshots
its deterministic iteration order. Duplicate module runtime types throw.
StackMutatable.pop completes onResult and calls onDidPop. Callers
of push no longer depend on a later Flutter page callback. A second
completeOnResult after pop throws. onDidPop is idempotent.
CoordinatorMutatable.pushOrMoveToTop returns Future<void>, aligned
with StackMutatable; await it when sequencing navigation operations.
defineLayout and defineConverter remain invoked by init() for
compatibility. Bind layouts on their paths with bindLayout, and register
restorable converters through defineRestorableConverter in init().RouteManifestLayoutKind is a sealed class.
Fixed children live on indexed(childIds) and branched(childIds).
Construct layouts with RouteManifestLayout.stack/indexed/branched so the
ID type is inferred from the node id. JSON still uses kind plus
indexedChildIds / branchChildIds.RouteManifestLayoutKind.branched(...)
declares an ordered set of direct child layout roots and validates that
every direct child belongs to the branch topology.RouteManifest,
with deterministic URI matching, layout relationship validation, ambiguous
pattern detection, composition, JSON serialization, and reverse routing.RouteManifest<I> and
RouteIdCodec<I>. RouteManifestMatch.id supports concise Dart object
patterns while codecs keep strings isolated to the JSON seam.RouteModule.routeManifest and
RouteManifestFragment. CoordinatorModular automatically flattens nested
fragments, validates the complete graph, preserves typed in-memory IDs, and
composes fragment-scoped JSON codecs. Hand-written parser-only coordinators
still default to RouteManifest.empty for compatibility.RouteBindingRegistry.
RouteModuleBinding is the single adapter (coordinator implements
RouteModule). It provides manifest-backed
URI parsing without handwritten parser switches while supporting sync,
async, not-found, and heterogeneous typed-ID bindings.
deferredBindingFactory / deferredRouteNotFoundBinding wrap any
factory so a deferred library can load before a match or not-found route
is created. RouteBinding.deferred uses the same wrapper.
RouteManifest.bind<T> builds a registry while inferring its ID type from
the manifest.Navigatable<T> and Mutatable<T> implemented by both
stack paths and coordinator mixins. Mutatable includes pushSilently in
addition to push, pushOrMoveToTop, and pushReplacement. pop stays
off the shared contract because path and coordinator return types differ.defineDeeplinkHandler: override built-in DeeplinkStrategy behaviour
(navigate / push / replace). custom still uses
RouteDeepLink.deeplinkHandler.navigateUri, pushUri,
pushSilentlyUri, replaceUri, recoverUri, pushReplacementUri, and
pushOrMoveToTopUri parse a URI before delegating to the corresponding route
operation. The same operations are also available on Uri
(uri.pushWith(coordinator), uri.navigateWith(coordinator),
uri.replaceWith(coordinator), …).markNeedRebuild on CoordinatorCore.pushSilently, allowing Router/deep-link code
to await stack commitment without waiting for a later pop result.StackMutatable.replaceAll,
preserving retained route lifecycles and emitting one committed state.push, replace, traverse, automatic)
is recorded in core and consumed by platform history adapters.RouteRequest, RouteResolver, and
typed match/redirect/not-found/error outcomes with status, headers, and
hydration data.NavigationCommit with a
monotonic revision, previous/final URI, and history intent for nested
mutations. isInNavigationTransaction lets paths notify synchronously during
multi-path operations; outside a transaction, deferred notifications remain
safe for rendering and headless consumers.RouteCancellationToken, propagated
across redirect requests and kept distinct from typed 500 failures.RouteHydrationPayload, with immutable
JSON-compatible data, schema validation, and encode/decode support.StackCommit seam,
avoiding a second redirect pass during a single navigation operation.pushReplacement
instead of leaving navigation at an intermediate popped state.navigate, so the intended instance is retained.onDidPop acknowledgements do not repeat lifecycle notifications.meta and collection);
Flutter and DartNative attach behavior through adapters and route bindings.`defineLayout` and `defineConverter` are deprecated. Bind layouts on the path with bindLayout (NavigationPath.createWith(...)..bindLayout(...)). Regis…
Prerelease for early testers. APIs may still change before 3.0.0.
Route manifests now describe topology only. RouteManifestRoute no
longer duplicates query, guard, redirect, deferred-loading, or deep-link
metadata from concrete route implementations.
Navigation operations moved out of CoordinatorCore into composable
capability mixins. CoordinatorCore is now a state container only
(paths, URI parsing, layout-parent hooks).
| Mixin | Capabilities |
|---|---|
CoordinatorLayoutCore |
Layout-parent registration / hierarchy activation |
CoordinatorNavigatable |
navigate |
CoordinatorMutatable |
push, pushSilently, pop, replace, pushReplacement, pushOrMoveToTop, tryPop |
CoordinatorRecoverable |
recover, recoverUri, defineDeeplinkHandler |
Flutter Coordinator still mixes all of them — existing apps that extend
Coordinator need no code changes. Custom CoordinatorCore subclasses must
mix in the capabilities they need.
Route value hashing now follows Dart's equality contract. Mutable path
and result lifecycle state no longer contributes to hashCode;
RouteTarget.deepEquals now means same lifecycle entry (reference identity).
Equatable.internalProps is removed. Equality and hashing use
runtimeType + props only. Delete any leftover subclass overrides.
CoordinatorModular.defineModules now returns Iterable and snapshots
its deterministic iteration order. Duplicate module runtime types throw.
defineLayout and defineConverter are deprecated. Bind layouts on
the path with bindLayout (NavigationPath.createWith(...)..bindLayout(...)).
Register restorable converters in init() via defineRestorableConverter.
init() still invokes both hooks for compatibility.
StackMutatable.pop completes onResult and calls onDidPop. Callers
of push no longer depend on a later Flutter page callback. A second
completeOnResult after pop throws. onDidPop is idempotent.
RouteManifestLayoutKind is a sealed class.
Fixed children live on indexed(childIds) and branched(childIds).
Construct layouts with RouteManifestLayout.stack/indexed/branched so the
ID type is inferred from the node id. JSON still uses kind plus
indexedChildIds / branchChildIds.RouteManifestLayoutKind.branched(...)
declares an ordered set of direct child layout roots and validates that
every direct child belongs to the branch topology.RouteManifest,
with deterministic URI matching, layout relationship validation, ambiguous
pattern detection, composition, JSON serialization, and reverse routing.RouteManifest<I> and
RouteIdCodec<I>. RouteManifestMatch.id supports concise Dart object
patterns while codecs keep strings isolated to the JSON seam.RouteModule.routeManifest and
RouteManifestFragment. CoordinatorModular automatically flattens nested
fragments, validates the complete graph, preserves typed in-memory IDs, and
composes fragment-scoped JSON codecs. Hand-written parser-only coordinators
still default to RouteManifest.empty for compatibility.RouteBindingRegistry.
RouteModuleBinding is the single adapter (coordinator implements
RouteModule). It provides manifest-backed
URI parsing without handwritten parser switches while supporting sync,
async, not-found, and heterogeneous typed-ID bindings.
deferredBindingFactory / deferredRouteNotFoundBinding wrap any
factory so a deferred library can load before a match or not-found route
is created. RouteBinding.deferred uses the same wrapper.
RouteManifest.bind<T> builds a registry while inferring its ID type from
the manifest.Navigatable<T> and Mutatable<T> implemented by both
stack paths and coordinator mixins. Mutatable includes pushSilently in
addition to push, pushOrMoveToTop, and pushReplacement. pop stays
off the shared contract because path and coordinator return types differ.StackCommit seam so coordinators apply an already-resolved
route without a second RouteRedirect pass. pushReplacement now falls
back to activateRoute when the parent path is not mutatable (indexed or
branched stacks).defineDeeplinkHandler: override built-in DeeplinkStrategy behaviour
(navigate / push / replace). custom still uses
RouteDeepLink.deeplinkHandler.navigateUri, pushUri,
pushSilentlyUri, replaceUri, recoverUri, pushReplacementUri, and
pushOrMoveToTopUri parse a URI before delegating to the corresponding route
operation. The same operations are also available on Uri
(uri.pushWith(coordinator), uri.navigateWith(coordinator),
uri.replaceWith(coordinator), …).markNeedRebuild on CoordinatorCore.CoordinatorMutatable.pushOrMoveToTop now returns Future<void>
(aligned with StackMutatable).pushSilently, allowing Router/deep-link code
to await stack commitment without waiting for a later pop result.StackMutatable.replaceAll,
preserving retained route lifecycles and emitting one committed state.push, replace, traverse, automatic)
is recorded in core and consumed by platform history adapters.RouteRequest, RouteResolver, and
typed match/redirect/not-found/error outcomes with status, headers, and
hydration data.NavigationCommit; concurrent top-level mutations serialize without being
mistaken for nested work. isInNavigationTransaction lets paths notify
synchronously inside a transaction so multi-path reset does not leak extra
commits. Sequential await of coordinator mutations restores queue idle in
the transaction finally and serializes overlapping work on a side
Completer, so the caller's future has no extra listeners. The trailing
microtask drain runs only when no path has already notified synchronously.
navigate prefers a matching lifecycle entry over the first value-equal
stack occupant, so updating an inactive instance does not pop and discard
that instance.RouteCancellationToken, propagated
across redirect requests and kept distinct from typed 500 failures.RouteHydrationPayload, with immutable
JSON-compatible data, schema validation, and encode/decode support.`GuardRule` contract renamed for coordinator-optional use. The 2.1.0 methods are removed:
GuardRule contract renamed for coordinator-optional use. The 2.1.0 methods are removed:
| Removed (2.2.0) | Replacement |
|---|---|
canPop(route) |
canPopRule(route) / canPopRuleWith(coordinator, route) |
canPopListenable(route) |
canPopListenableRule(route) / canPopListenableRuleWith(coordinator, route) |
guard(coordinator, route) |
guardRule(route) / guardRuleWith(coordinator, route) |
Migration:
// Before (2.2.0)
class UnsavedChangesRule extends GuardRule<AppRoute> {
@override
bool canPop(AppRoute route) => !route.hasUnsavedChanges;
@override
FutureOr<bool?> guard(CoordinatorCore c, AppRoute route) async { /* ... */ }
}
// After (2.3.0) — route-only
class UnsavedChangesRule extends GuardRule<AppRoute> {
@override
bool canPopRule(AppRoute route) => !route.hasUnsavedChanges;
@override
FutureOr<bool?> guardRule(AppRoute route) async { /* ... */ }
}
// After (2.3.0) — needs coordinator (dialogs, app state)
class UnsavedChangesRule extends GuardRule<AppRoute> {
@override
bool canPopRule(AppRoute route) => !route.hasUnsavedChanges;
@override
FutureOr<bool?> guardRuleWith(CoordinatorCore c, AppRoute route) async { /* ... */ }
}
Each *With method defaults to its non-With counterpart. guardRule defaults to null (continue chain).
RouteGuard.canPopWith / canPopListenableWith: Coordinator-aware PopScope hints (default to canPop / canPopListenable).RouteGuardRule.popGuard: Now runs the guardRule chain (no coordinator), matching popGuardWith → guardRuleWith.Nothing published for this version
chore: make RedirectRule can be const
RedirectRule can be constFix CoordinatorModular.getModule now correctly resolves the coordinator itself by registering runtimeType: this in _allModules, enabling getModule ()
CoordinatorModular.getModule now correctly resolves the coordinator itself by registering runtimeType: this in _allModules, enabling getModule<MyCoordinator>() to work at any level of the hierarchy.Fix CoordinatorModular edge case cascading dispose and prevent duplicate definitions.
CoordinatorModular edge case cascading dispose and prevent duplicate definitions.Extract core function from zenrouter package
zenrouter packageYour coding agent can read these notes before it upgrades. Set up the MCP server →