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: 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/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 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