zenrouter_core 3.0.0
zenrouter_core: ^3.0.0 copied to clipboard
Headless routing engine for ZenRouter family.
3.0.0 #
Stable adapter-neutral routing engine. The changes below are cumulative since 2.3.0 and apply to both Flutter and DartNative adapters.
Breaking changes #
-
Route manifests now describe topology only.
RouteManifestRouteno longer duplicates query, guard, redirect, deferred-loading, or deep-link metadata from concrete route implementations. -
Navigation operations moved out of
CoordinatorCoreinto composable capability mixins.CoordinatorCoreis now a state container only (paths, URI parsing, layout-parent hooks).Mixin Capabilities CoordinatorLayoutCoreLayout-parent registration / hierarchy activation CoordinatorNavigatablenavigateCoordinatorMutatablepush,pushSilently,pop,replace,pushReplacement,pushOrMoveToTop,tryPopCoordinatorRecoverablerecover,recoverUri,defineDeeplinkHandlerFlutter
Coordinatorstill composes the full capability set. CustomCoordinatorCoresubclasses 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.deepEqualsnow means same lifecycle entry (reference identity). -
Equatable.internalPropsis removed. Equality and hashing useruntimeType+propsonly. Delete any leftover subclass overrides. -
CoordinatorModular.defineModulesnow returnsIterableand snapshots its deterministic iteration order. Duplicate module runtime types throw. -
StackMutatable.popcompletesonResultand callsonDidPop. Callers ofpushno longer depend on a later Flutter page callback. A secondcompleteOnResultafterpopthrows.onDidPopis idempotent. -
CoordinatorMutatable.pushOrMoveToTopreturnsFuture<void>, aligned withStackMutatable; await it when sequencing navigation operations.
Deprecated #
defineLayoutanddefineConverterremain invoked byinit()for compatibility. Bind layouts on their paths withbindLayout, and register restorable converters throughdefineRestorableConverterininit().
Added #
- Sealed layout kinds:
RouteManifestLayoutKindis a sealed class. Fixed children live onindexed(childIds)andbranched(childIds). Construct layouts withRouteManifestLayout.stack/indexed/branchedso the ID type is inferred from the node id. JSON still useskindplusindexedChildIds/branchChildIds. - Branched manifest layouts:
RouteManifestLayoutKind.branched(...)declares an ordered set of direct child layout roots and validates that every direct child belongs to the branch topology. - Declarative route graph via the immutable, versioned
RouteManifest, with deterministic URI matching, layout relationship validation, ambiguous pattern detection, composition, JSON serialization, and reverse routing. - Strongly typed manifest IDs via
RouteManifest<I>andRouteIdCodec<I>.RouteManifestMatch.idsupports concise Dart object patterns while codecs keep strings isolated to the JSON seam. - Manifest seam for route modules via
RouteModule.routeManifestandRouteManifestFragment.CoordinatorModularautomatically 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 toRouteManifest.emptyfor compatibility. - Runtime Route Bindings via the validated
RouteBindingRegistry.RouteModuleBindingis the single adapter (coordinator implementsRouteModule). It provides manifest-backed URI parsing without handwritten parser switches while supporting sync, async, not-found, and heterogeneous typed-ID bindings.deferredBindingFactory/deferredRouteNotFoundBindingwrap any factory so a deferred library can load before a match or not-found route is created.RouteBinding.deferreduses the same wrapper.RouteManifest.bind<T>builds a registry while inferring its ID type from the manifest. - Shared contracts
Navigatable<T>andMutatable<T>implemented by both stack paths and coordinator mixins.MutatableincludespushSilentlyin addition topush,pushOrMoveToTop, andpushReplacement.popstays off the shared contract because path and coordinator return types differ. defineDeeplinkHandler: override built-inDeeplinkStrategybehaviour (navigate/push/replace).customstill usesRouteDeepLink.deeplinkHandler.- URI-based coordinator actions:
navigateUri,pushUri,pushSilentlyUri,replaceUri,recoverUri,pushReplacementUri, andpushOrMoveToTopUriparse a URI before delegating to the corresponding route operation. The same operations are also available onUri(uri.pushWith(coordinator),uri.navigateWith(coordinator),uri.replaceWith(coordinator), …). markNeedRebuildonCoordinatorCore.- Commit-only navigation via
pushSilently, allowing Router/deep-link code to await stack commitment without waiting for a later pop result. - Atomic declarative stack replacement via
StackMutatable.replaceAll, preserving retained route lifecycles and emitting one committed state. - Navigation history intent (
push,replace,traverse,automatic) is recorded in core and consumed by platform history adapters. - Adapter-neutral route resolution:
RouteRequest,RouteResolver, and typed match/redirect/not-found/error outcomes with status, headers, and hydration data. - Atomic coordinator transactions publish one
NavigationCommitwith a monotonic revision, previous/final URI, and history intent for nested mutations.isInNavigationTransactionlets paths notify synchronously during multi-path operations; outside a transaction, deferred notifications remain safe for rendering and headless consumers. - Cooperative route cancellation via
RouteCancellationToken, propagated across redirect requests and kept distinct from typed 500 failures. - Redirect continuation semantics for 301, 302, 303, 307, and 308, including method/body/header handling and relative-location resolution.
- Versioned hydration envelope via
RouteHydrationPayload, with immutable JSON-compatible data, schema validation, and encode/decode support.
Fixed #
- Apply already-resolved routes through the internal
StackCommitseam, avoiding a second redirect pass during a single navigation operation. - Activate destinations on indexed/branched parents during
pushReplacementinstead of leaving navigation at an intermediate popped state. - Match an existing route by lifecycle identity before choosing a value-equal
entry during
navigate, so the intended instance is retained. - Keep independent top-level asynchronous mutations serialized; only actual nested mutations share a transaction. Multi-path resets publish one final commit, and awaited operations leave the transaction queue ready for reuse.
- Complete pop results and cleanup without depending on a platform callback.
Repeated
onDidPopacknowledgements do not repeat lifecycle notifications.
Requirements #
- Dart 3.8 or newer.
- Runtime dependencies remain platform-neutral (
metaandcollection); Flutter and DartNative attach behavior through adapters and route bindings.
3.0.0-beta.1 #
Prerelease for early testers. APIs may still change before 3.0.0.
Breaking Changes #
-
Route manifests now describe topology only.
RouteManifestRouteno longer duplicates query, guard, redirect, deferred-loading, or deep-link metadata from concrete route implementations. -
Navigation operations moved out of
CoordinatorCoreinto composable capability mixins.CoordinatorCoreis now a state container only (paths, URI parsing, layout-parent hooks).Mixin Capabilities CoordinatorLayoutCoreLayout-parent registration / hierarchy activation CoordinatorNavigatablenavigateCoordinatorMutatablepush,pushSilently,pop,replace,pushReplacement,pushOrMoveToTop,tryPopCoordinatorRecoverablerecover,recoverUri,defineDeeplinkHandlerFlutter
Coordinatorstill mixes all of them — existing apps that extendCoordinatorneed no code changes. CustomCoordinatorCoresubclasses 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.deepEqualsnow means same lifecycle entry (reference identity). -
Equatable.internalPropsis removed. Equality and hashing useruntimeType+propsonly. Delete any leftover subclass overrides. -
CoordinatorModular.defineModulesnow returnsIterableand snapshots its deterministic iteration order. Duplicate module runtime types throw. -
defineLayoutanddefineConverterare deprecated. Bind layouts on the path withbindLayout(NavigationPath.createWith(...)..bindLayout(...)). Register restorable converters ininit()viadefineRestorableConverter.init()still invokes both hooks for compatibility. -
StackMutatable.popcompletesonResultand callsonDidPop. Callers ofpushno longer depend on a later Flutter page callback. A secondcompleteOnResultafterpopthrows.onDidPopis idempotent.
New Features #
- Sealed layout kinds:
RouteManifestLayoutKindis a sealed class. Fixed children live onindexed(childIds)andbranched(childIds). Construct layouts withRouteManifestLayout.stack/indexed/branchedso the ID type is inferred from the node id. JSON still useskindplusindexedChildIds/branchChildIds. - Branched manifest layouts:
RouteManifestLayoutKind.branched(...)declares an ordered set of direct child layout roots and validates that every direct child belongs to the branch topology. - Declarative route graph via the immutable, versioned
RouteManifest, with deterministic URI matching, layout relationship validation, ambiguous pattern detection, composition, JSON serialization, and reverse routing. - Strongly typed manifest IDs via
RouteManifest<I>andRouteIdCodec<I>.RouteManifestMatch.idsupports concise Dart object patterns while codecs keep strings isolated to the JSON seam. - Manifest seam for route modules via
RouteModule.routeManifestandRouteManifestFragment.CoordinatorModularautomatically 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 toRouteManifest.emptyfor compatibility. - Runtime Route Bindings via the validated
RouteBindingRegistry.RouteModuleBindingis the single adapter (coordinator implementsRouteModule). It provides manifest-backed URI parsing without handwritten parser switches while supporting sync, async, not-found, and heterogeneous typed-ID bindings.deferredBindingFactory/deferredRouteNotFoundBindingwrap any factory so a deferred library can load before a match or not-found route is created.RouteBinding.deferreduses the same wrapper.RouteManifest.bind<T>builds a registry while inferring its ID type from the manifest. - Shared contracts
Navigatable<T>andMutatable<T>implemented by both stack paths and coordinator mixins.MutatableincludespushSilentlyin addition topush,pushOrMoveToTop, andpushReplacement.popstays off the shared contract because path and coordinator return types differ. - Internal
StackCommitseam so coordinators apply an already-resolved route without a secondRouteRedirectpass.pushReplacementnow falls back toactivateRoutewhen the parent path is not mutatable (indexed or branched stacks). defineDeeplinkHandler: override built-inDeeplinkStrategybehaviour (navigate/push/replace).customstill usesRouteDeepLink.deeplinkHandler.- URI-based coordinator actions:
navigateUri,pushUri,pushSilentlyUri,replaceUri,recoverUri,pushReplacementUri, andpushOrMoveToTopUriparse a URI before delegating to the corresponding route operation. The same operations are also available onUri(uri.pushWith(coordinator),uri.navigateWith(coordinator),uri.replaceWith(coordinator), …). markNeedRebuildonCoordinatorCore.CoordinatorMutatable.pushOrMoveToTopnow returnsFuture<void>(aligned withStackMutatable).- Commit-only navigation via
pushSilently, allowing Router/deep-link code to await stack commitment without waiting for a later pop result. - Atomic declarative stack replacement via
StackMutatable.replaceAll, preserving retained route lifecycles and emitting one committed state. - Navigation history intent (
push,replace,traverse,automatic) is recorded in core and consumed by platform history adapters. - Adapter-neutral route resolution:
RouteRequest,RouteResolver, and typed match/redirect/not-found/error outcomes with status, headers, and hydration data. - Atomic coordinator transactions: nested mutations publish one
NavigationCommit; concurrent top-level mutations serialize without being mistaken for nested work.isInNavigationTransactionlets paths notify synchronously inside a transaction so multi-pathresetdoes not leak extra commits. Sequentialawaitof coordinator mutations restores queue idle in the transactionfinallyand 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.navigateprefers a matching lifecycle entry over the first value-equal stack occupant, so updating an inactive instance does not pop and discard that instance. - Cooperative route cancellation via
RouteCancellationToken, propagated across redirect requests and kept distinct from typed 500 failures. - Redirect continuation semantics for 301, 302, 303, 307, and 308, including method/body/header handling and relative-location resolution.
- Versioned hydration envelope via
RouteHydrationPayload, with immutable JSON-compatible data, schema validation, and encode/decode support.
2.3.0 #
Breaking Changes #
-
GuardRulecontract 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
*Withmethod defaults to its non-Withcounterpart.guardRuledefaults tonull(continue chain).
New Features #
RouteGuard.canPopWith/canPopListenableWith: Coordinator-aware PopScope hints (default tocanPop/canPopListenable).RouteGuardRule.popGuard: Now runs theguardRulechain (no coordinator), matchingpopGuardWith→guardRuleWith.
2.2.0 #
New Features #
GuardRule/RouteGuardRule: Composable pop-guard chains (first non-nullboolwins), mirroringRedirectRule/RouteRedirectRule.RouteGuard.canPop/canPopListenable: Sync PopScope hint plus optionalListenableMixininvalidation when leave-safety changes.ListenableMixin: Subscribe-only reactive surface (withListenableMixin.merge);ListenableObjectnow implements it.
Breaking Changes #
CoordinatorCore.pop: Pops only the nearest eligible stack path. Nested shells are no longer popped together with child stacks in a single call.RouteRedirect.resolve: ThrowsStateErrorwhen a redirect returns a different route type (previously silently ignored).
2.0.3 #
- chore: make
RedirectRulecan be const
2.0.2 #
- Fix
CoordinatorModular.getModulenow correctly resolves the coordinator itself by registeringruntimeType: thisin_allModules, enablinggetModule<MyCoordinator>()to work at any level of the hierarchy.
2.0.1 #
- Fix
CoordinatorModularedge case cascading dispose and prevent duplicate definitions.
2.0.0 #
- Extract core function from
zenrouterpackage