From 796f6e80884d85577f31c558d66a87195abd51b7 Mon Sep 17 00:00:00 2001 From: definev Date: Mon, 2 Mar 2026 10:40:30 +0700 Subject: [PATCH 1/3] fix(modular): fix `CoordinatorModular` edgecase cascading dispose and prevent duplicate definitions. --- .../test/coordinator/nested_modular_test.dart | 107 ++++++++++++++++++ .../zenrouter/test/mixin/layout_test.dart | 27 +++++ .../lib/src/coordinator/modular.dart | 26 ++++- packages/zenrouter_core/pubspec.yaml | 2 +- 4 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 packages/zenrouter/test/coordinator/nested_modular_test.dart diff --git a/packages/zenrouter/test/coordinator/nested_modular_test.dart b/packages/zenrouter/test/coordinator/nested_modular_test.dart new file mode 100644 index 0000000..19b810f --- /dev/null +++ b/packages/zenrouter/test/coordinator/nested_modular_test.dart @@ -0,0 +1,107 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zenrouter/zenrouter.dart'; + +abstract class AppRoute extends RouteTarget with RouteUnique { + @override + Uri toUri(); + + @override + Widget build(covariant Coordinator coordinator, BuildContext context) { + return const SizedBox(); + } + + @override + List get props => []; +} + +class LeafRoute extends AppRoute { + @override + Uri toUri() => Uri.parse('/leaf'); +} + +class LeafModule extends RouteModule { + LeafModule(super.coordinator); + + int layoutCount = 0; + int converterCount = 0; + + @override + void defineLayout() { + layoutCount++; + super.defineLayout(); + } + + @override + void defineConverter() { + converterCount++; + super.defineConverter(); + } + + late final NavigationPath leafPath = NavigationPath.createWith( + label: 'leaf', + coordinator: coordinator as Coordinator, + ); + + @override + List get paths => [leafPath]; + + @override + FutureOr parseRouteFromUri(Uri uri) { + if (uri.path == '/leaf') return LeafRoute(); + return null; + } +} + +class SubCoordinator extends Coordinator + with CoordinatorModular { + SubCoordinator(this.coordinator); + + @override + final CoordinatorModular coordinator; + + @override + Set> defineModules() => {LeafModule(this)}; + + @override + AppRoute notFoundRoute(Uri uri) => throw UnimplementedError(); +} + +class RootCoordinator extends Coordinator + with CoordinatorModular { + @override + Set> defineModules() => {SubCoordinator(this)}; + + @override + AppRoute notFoundRoute(Uri uri) => throw UnimplementedError(); +} + +void main() { + test('Nested CoordinatorModular double inclusion/execution', () { + final root = RootCoordinator(); + + final leaf = root.getModule(); + + final paths = root.paths; + final leafPaths = paths.where((p) => p == leaf.leafPath).toList(); + + expect(paths.length, 2); + expect( + leafPaths.length, + 1, + reason: 'Leaf path was included multiple times in root paths', + ); + expect( + leaf.layoutCount, + 1, + reason: 'defineLayout was called multiple times on leaf module', + ); + expect( + leaf.converterCount, + 1, + reason: 'defineConverter was called multiple times on leaf module', + ); + }); +} diff --git a/packages/zenrouter/test/mixin/layout_test.dart b/packages/zenrouter/test/mixin/layout_test.dart index 5ab256f..7e921f3 100644 --- a/packages/zenrouter/test/mixin/layout_test.dart +++ b/packages/zenrouter/test/mixin/layout_test.dart @@ -210,6 +210,13 @@ class _ChildModuleCoordinator extends Coordinator { @override LayoutTestRoute parseRouteFromUri(Uri uri) => HomeRoute(); + + bool isDisposed = false; + @override + void dispose() { + isDisposed = true; + super.dispose(); + } } class _ParentModularCoordinator extends Coordinator @@ -221,6 +228,13 @@ class _ParentModularCoordinator extends Coordinator @override LayoutTestRoute notFoundRoute(Uri uri) => HomeRoute(); + + bool isDisposed = false; + @override + void dispose() { + isDisposed = true; + super.dispose(); + } } // ============================================================================ @@ -386,5 +400,18 @@ void main() { isFalse, ); }); + + test('CoordinatorModular dispose cascade', () { + final parent = _ParentModularCoordinator(); + final child = parent.getModule<_ChildModuleCoordinator>(); + + expect(child.isDisposed, isFalse); + expect(parent.isDisposed, isFalse); + + parent.dispose(); + + expect(child.isDisposed, isTrue); + expect(parent.isDisposed, isTrue); + }); }); } diff --git a/packages/zenrouter_core/lib/src/coordinator/modular.dart b/packages/zenrouter_core/lib/src/coordinator/modular.dart index 0014784..ae82341 100644 --- a/packages/zenrouter_core/lib/src/coordinator/modular.dart +++ b/packages/zenrouter_core/lib/src/coordinator/modular.dart @@ -75,6 +75,22 @@ mixin CoordinatorModular on CoordinatorCore { for (final module in defineModules()) module.runtimeType: module, }; + late final Map> _allModules = { + ..._modules, + for (final module in _modules.values) + if (module case CoordinatorModular modular) ...modular._allModules.cast(), + }; + + @override + void dispose() { + for (final module in _modules.values.whereType()) { + module.dispose(); + } + _modules.clear(); + _allModules.clear(); + super.dispose(); + } + /// Returns the set of route modules for this coordinator. /// /// The order determines which module is checked first during route parsing. @@ -83,7 +99,7 @@ mixin CoordinatorModular on CoordinatorCore { /// Retrieves a module by its type. /// /// Throws [TypeError] if the module is not registered. - R getModule>() => _modules[R] as R; + R getModule>() => _allModules[R] as R; @override List> get paths => [ @@ -100,7 +116,9 @@ mixin CoordinatorModular on CoordinatorCore { void defineLayout() { super.defineLayout(); for (final module in _modules.values) { - module.defineLayout(); + if (module is! CoordinatorCore) { + module.defineLayout(); + } } } @@ -108,7 +126,9 @@ mixin CoordinatorModular on CoordinatorCore { void defineConverter() { super.defineConverter(); for (final module in _modules.values) { - module.defineConverter(); + if (module is! CoordinatorCore) { + module.defineConverter(); + } } } diff --git a/packages/zenrouter_core/pubspec.yaml b/packages/zenrouter_core/pubspec.yaml index 148fc2d..9855fd1 100644 --- a/packages/zenrouter_core/pubspec.yaml +++ b/packages/zenrouter_core/pubspec.yaml @@ -2,7 +2,7 @@ name: zenrouter_core description: >- The core routing engine and unified interfaces for the ZenRouter navigation system. Contains common models, types, and logic used across the zenrouter ecosystem. -version: 2.0.0 +version: 2.0.1 repository: https://github.com/definev/zenrouter homepage: https://github.com/definev/zenrouter/tree/main/packages/zenrouter_core From ad4ef47ee8be50ef62ca9c37abf30c77a5d425ec Mon Sep 17 00:00:00 2001 From: definev Date: Mon, 2 Mar 2026 10:46:06 +0700 Subject: [PATCH 2/3] docs: add changelog for zenrouter_core 2.0.1 --- packages/zenrouter_core/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/zenrouter_core/CHANGELOG.md b/packages/zenrouter_core/CHANGELOG.md index bcc6432..e0df245 100644 --- a/packages/zenrouter_core/CHANGELOG.md +++ b/packages/zenrouter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.1 + +- Fix `CoordinatorModular` edge case cascading dispose and prevent duplicate definitions. + ## 2.0.0 - Extract core function from `zenrouter` package \ No newline at end of file From 9585752290f242749d3d69086e62b262a4d824da Mon Sep 17 00:00:00 2001 From: definev Date: Mon, 2 Mar 2026 10:46:32 +0700 Subject: [PATCH 3/3] fix: address CoordinatorModular dispose edgecase and update zenrouter_core to 2.0.1. --- packages/zenrouter/CHANGELOG.md | 3 +++ packages/zenrouter/pubspec.yaml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/zenrouter/CHANGELOG.md b/packages/zenrouter/CHANGELOG.md index b0912eb..c462063 100644 --- a/packages/zenrouter/CHANGELOG.md +++ b/packages/zenrouter/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.0.2 +- **Fix**: Fix `CoordinatorModular` edgecase cascading dispose and prevent duplicate definitions. (Bumped `zenrouter_core` to 2.0.1) + ## 2.0.1 - **Fix**: Revert `hasEmptyPath` back to `pathSegments.isEmpty` in `resolveInitialUri` for correct path empty checks. - **Refactor**: Remove redundant `initialRouteInformation` parameter from `CoordinatorRouteInformationProvider` since fallback defaults and resolution logic is robust now. diff --git a/packages/zenrouter/pubspec.yaml b/packages/zenrouter/pubspec.yaml index e3cd1b9..b6cb4b0 100644 --- a/packages/zenrouter/pubspec.yaml +++ b/packages/zenrouter/pubspec.yaml @@ -2,7 +2,7 @@ name: zenrouter description: >- A powerful Flutter router with deep linking, web support, type-safe routing, guards, redirects, and zero boilerplate. -version: 2.0.1 +version: 2.0.2 repository: https://github.com/definev/zenrouter homepage: https://github.com/definev/zenrouter/tree/main/packages/zenrouter @@ -30,7 +30,7 @@ dependencies: flutter: sdk: flutter collection: ^1.19.1 - zenrouter_core: ^2.0.0 + zenrouter_core: ^2.0.1 dev_dependencies: flutter_test: