Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/zenrouter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/zenrouter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
107 changes: 107 additions & 0 deletions packages/zenrouter/test/coordinator/nested_modular_test.dart
Original file line number Diff line number Diff line change
@@ -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<Object?> get props => [];
}

class LeafRoute extends AppRoute {
@override
Uri toUri() => Uri.parse('/leaf');
}

class LeafModule extends RouteModule<AppRoute> {
LeafModule(super.coordinator);

int layoutCount = 0;
int converterCount = 0;

@override
void defineLayout() {
layoutCount++;
super.defineLayout();
}

@override
void defineConverter() {
converterCount++;
super.defineConverter();
}

late final NavigationPath<AppRoute> leafPath = NavigationPath.createWith(
label: 'leaf',
coordinator: coordinator as Coordinator<AppRoute>,
);

@override
List<StackPath> get paths => [leafPath];

@override
FutureOr<AppRoute?> parseRouteFromUri(Uri uri) {
if (uri.path == '/leaf') return LeafRoute();
return null;
}
}

class SubCoordinator extends Coordinator<AppRoute>
with CoordinatorModular<AppRoute> {
SubCoordinator(this.coordinator);

@override
final CoordinatorModular<AppRoute> coordinator;

@override
Set<RouteModule<AppRoute>> defineModules() => {LeafModule(this)};

@override
AppRoute notFoundRoute(Uri uri) => throw UnimplementedError();
}

class RootCoordinator extends Coordinator<AppRoute>
with CoordinatorModular<AppRoute> {
@override
Set<RouteModule<AppRoute>> 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<LeafModule>();

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',
);
});
}
27 changes: 27 additions & 0 deletions packages/zenrouter/test/mixin/layout_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ class _ChildModuleCoordinator extends Coordinator<LayoutTestRoute> {

@override
LayoutTestRoute parseRouteFromUri(Uri uri) => HomeRoute();

bool isDisposed = false;
@override
void dispose() {
isDisposed = true;
super.dispose();
}
}

class _ParentModularCoordinator extends Coordinator<LayoutTestRoute>
Expand All @@ -221,6 +228,13 @@ class _ParentModularCoordinator extends Coordinator<LayoutTestRoute>

@override
LayoutTestRoute notFoundRoute(Uri uri) => HomeRoute();

bool isDisposed = false;
@override
void dispose() {
isDisposed = true;
super.dispose();
}
}

// ============================================================================
Expand Down Expand Up @@ -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);
});
});
}
4 changes: 4 additions & 0 deletions packages/zenrouter_core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
26 changes: 23 additions & 3 deletions packages/zenrouter_core/lib/src/coordinator/modular.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ mixin CoordinatorModular<T extends RouteUri> on CoordinatorCore<T> {
for (final module in defineModules()) module.runtimeType: module,
};

late final Map<Type, RouteModule<T>> _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<CoordinatorCore>()) {
module.dispose();
}
_modules.clear();
_allModules.clear();
super.dispose();
Comment on lines +86 to +91
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In dispose(), _modules is cleared before calling super.dispose(). Since CoordinatorModular.paths depends on _modules, clearing it first means CoordinatorCore.dispose() will no longer see module paths and therefore won’t remove listeners / dispose those paths. Fix by calling super.dispose() before clearing _modules/_allModules, or by snapshotting the current paths/module paths before clearing and disposing them explicitly.

Suggested change
for (final module in _modules.values.whereType<CoordinatorCore>()) {
module.dispose();
}
_modules.clear();
_allModules.clear();
super.dispose();
// Ensure CoordinatorCore.dispose() can still see all module paths via `paths`.
super.dispose();
// Then dispose any modules that are also CoordinatorCore instances.
for (final module in _modules.values.whereType<CoordinatorCore>()) {
module.dispose();
}
// Finally, clear module registries to release references.
_modules.clear();
_allModules.clear();

Copilot uses AI. Check for mistakes.
}

/// Returns the set of route modules for this coordinator.
///
/// The order determines which module is checked first during route parsing.
Expand All @@ -83,7 +99,7 @@ mixin CoordinatorModular<T extends RouteUri> on CoordinatorCore<T> {
/// Retrieves a module by its type.
///
/// Throws [TypeError] if the module is not registered.
R getModule<R extends RouteModule<T>>() => _modules[R] as R;
R getModule<R extends RouteModule<T>>() => _allModules[R] as R;

@override
List<StackPath<RouteTarget>> get paths => [
Expand All @@ -100,15 +116,19 @@ mixin CoordinatorModular<T extends RouteUri> on CoordinatorCore<T> {
void defineLayout() {
super.defineLayout();
for (final module in _modules.values) {
module.defineLayout();
if (module is! CoordinatorCore) {
module.defineLayout();
}
}
}

@override
void defineConverter() {
super.defineConverter();
for (final module in _modules.values) {
module.defineConverter();
if (module is! CoordinatorCore) {
module.defineConverter();
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/zenrouter_core/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down