Skip to content
4 changes: 4 additions & 0 deletions packages/zenrouter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.0.1
- **Fix**: Revert `hasEmptyPath` back to `pathSegments.isEmpty` in `resolveInitialUri` for correct path empty checks.
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

CHANGELOG entry says hasEmptyPath was reverted back to pathSegments.isEmpty in resolveInitialUri, but the current implementation still uses hasEmptyPath (for the empty-path normalization branch). Please adjust the wording to match what the code actually does so readers aren’t misled.

Suggested change
- **Fix**: Revert `hasEmptyPath` back to `pathSegments.isEmpty` in `resolveInitialUri` for correct path empty checks.
- **Fix**: Correct empty-path normalization logic in `resolveInitialUri` to ensure accurate empty-path checks.

Copilot uses AI. Check for mistakes.
- **Refactor**: Remove redundant `initialRouteInformation` parameter from `CoordinatorRouteInformationProvider` since fallback defaults and resolution logic is robust now.

## 2.0.0

🎉 **Major Release - Core Architecture & Layouts**
Expand Down
10 changes: 4 additions & 6 deletions packages/zenrouter/lib/src/coordinator/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,12 @@ abstract class Coordinator<T extends RouteUnique> extends CoordinatorCore<T>
/// The [RouteInformationProvider] that is used to configure the [Router].
///
/// ## Relationship
/// Supplies the initial URI from [initialRoutePath] or defaults to `/`.
/// Supplies the initial URI, preferring any platform-provided route
/// (e.g. from [PlatformDispatcher.defaultRouteName]), then falling back to
/// [initialRoutePath] if set, and finally defaulting to `/`.
@override
late final RouteInformationProvider routeInformationProvider =
PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(
uri: initialRoutePath ?? Uri.parse('/'),
),
);
CoordinatorRouteInformationProvider(coordinator: this);
Comment on lines 238 to +239
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The PR description mentions adding a private _resolveInitialUri method on Coordinator, but the implementation introduces CoordinatorRouteInformationProvider.resolveInitialUri instead. Please update the PR description (or move the helper onto Coordinator if that was the intent) so the documentation matches the actual change.

Copilot uses AI. Check for mistakes.

/// Access to the navigator state.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/widgets.dart';
import 'package:zenrouter/zenrouter.dart';

Comment thread
definev marked this conversation as resolved.
/// A [RouteInformationProvider] that derives its initial route from both the
/// platform and a [Coordinator].
///
/// The initial [Uri] is resolved by first parsing the platform dispatcher’s
/// [defaultRouteName]. If that route has no path segments (is effectively
/// empty) and [Coordinator.initialRoutePath] is non-null, the coordinator’s
/// [initialRoutePath] is used instead. When an initial URI is chosen but has
/// an empty path, it is normalized to `/`, and if no usable platform route
/// can be parsed the URI defaults to `/`.
class CoordinatorRouteInformationProvider
extends PlatformRouteInformationProvider {
CoordinatorRouteInformationProvider({required Coordinator coordinator})
: _coordinator = coordinator,
super(
initialRouteInformation: RouteInformation(
uri: resolveInitialUri(
WidgetsBinding.instance.platformDispatcher.defaultRouteName,
coordinator.initialRoutePath,
),
),
);

final Coordinator _coordinator;

Coordinator get coordinator => _coordinator;

@visibleForTesting
static Uri resolveInitialUri(String? platformRouteName, Uri? initialUri) {
final defaultUri = Uri.tryParse(platformRouteName ?? '');

// If the platform route name can't be parsed, fall back to the provided
// initialUri when available; otherwise, use the root route.
if (defaultUri == null) {
return initialUri ?? Uri.parse('/');
}

if (defaultUri.pathSegments.isEmpty && initialUri != null) {
return initialUri;
}

if (defaultUri.hasEmptyPath) {
return defaultUri.replace(path: '/');
}

return defaultUri;
}
}
1 change: 1 addition & 0 deletions packages/zenrouter/lib/zenrouter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export 'src/coordinator/observer.dart';
export 'src/coordinator/router.dart';
export 'src/coordinator/transition.dart';
export 'src/coordinator/restoration/_public.dart';
export 'src/coordinator/route_information_provider.dart';

/// Path base
export 'src/path/layout.dart';
Expand Down
2 changes: 1 addition & 1 deletion 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.0
version: 2.0.1

repository: https://github.com/definev/zenrouter
homepage: https://github.com/definev/zenrouter/tree/main/packages/zenrouter
Expand Down
3 changes: 1 addition & 2 deletions packages/zenrouter/test/coordinator/restoration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -855,9 +855,8 @@ void main() {
final coordinator = TestCoordinator(
initialRoutePath: Uri.parse('/settings'),
);
final config = coordinator;

await tester.pumpWidget(MaterialApp.router(routerConfig: config));
await tester.pumpWidget(MaterialApp.router(routerConfig: coordinator));

await tester.pumpAndSettle();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:zenrouter/zenrouter.dart';

abstract class AppRoute extends RouteTarget with RouteUnique {
@override
Uri toUri();
}

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

@override
Widget build(covariant TestCoordinator coordinator, BuildContext context) {
return const Scaffold(body: Text('Home'));
}

@override
List<Object?> get props => [];
}

class TestCoordinator extends Coordinator<AppRoute> {
TestCoordinator({super.initialRoutePath});

@override
AppRoute parseRouteFromUri(Uri uri) {
return HomeRoute();
}
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

group('CoordinatorRouteInformationProvider', () {
test('creates with coordinator and default initial route', () {
final coordinator = TestCoordinator();
final provider = CoordinatorRouteInformationProvider(
coordinator: coordinator,
);

expect(provider.coordinator, equals(coordinator));
expect(provider.value.uri.toString(), equals('/'));
});

test('reports new route information', () {
final coordinator = TestCoordinator();
final provider = CoordinatorRouteInformationProvider(
coordinator: coordinator,
);

provider.routerReportsNewRouteInformation(
RouteInformation(uri: Uri.parse('/new-route')),
);

expect(provider.value.uri.toString(), equals('/new-route'));
});

test('inherits from PlatformRouteInformationProvider', () {
final coordinator = TestCoordinator();
final provider = CoordinatorRouteInformationProvider(
coordinator: coordinator,
);

expect(provider, isA<PlatformRouteInformationProvider>());
expect(provider, isA<RouteInformationProvider>());
});
});

group('resolveInitialUri', () {
test('returns "/" when platformRouteName and initialUri are null', () {
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
null,
null,
);
expect(result.toString(), equals('/'));
});

test(
'returns "/" when platformRouteName is empty and initialUri is null',
() {
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
'',
null,
);
expect(result.toString(), equals('/'));
},
);

test(
'returns initialUri when platformRouteName is empty and initialUri is provided',
() {
final initialUri = Uri.parse('/initial');
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
'',
initialUri,
);
expect(result, equals(initialUri));
},
);

test(
'returns defaultUri with "/" path when platformRouteName has empty path and initialUri is null',
() {
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
'https://example.com',
null,
);
expect(result.toString(), equals('https://example.com/'));
},
);

test(
'returns initialUri when platformRouteName has empty path and initialUri is provided',
() {
final initialUri = Uri.parse('/custom');
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
'https://example.com',
initialUri,
);
expect(result, equals(initialUri));
},
);

test(
'returns platformRouteName when it has non-empty path and initialUri is null',
() {
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
'/platform',
null,
);
expect(result.toString(), equals('/platform'));
},
);

test(
'returns platformRouteName when it has non-empty path and initialUri is provided',
() {
final initialUri = Uri.parse('/initial');
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
'/platform',
initialUri,
);
expect(result.toString(), equals('/platform'));
},
);

test(
'returns "/" when platformRouteName is invalid and initialUri is null',
() {
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
'::invalid::',
null,
);
expect(result.toString(), equals('/'));
},
);

test(
'returns "/" when platformRouteName is invalid and initialUri is provided',
() {
final initialUri = Uri.parse('/initial');
final result = CoordinatorRouteInformationProvider.resolveInitialUri(
'::invalid::',
initialUri,
);
expect(result.toString(), equals('/initial'));
},
);
});
}