From 3903d8867de8b0f1fcea0348ceaf6cd433e97e07 Mon Sep 17 00:00:00 2001 From: definev Date: Fri, 27 Feb 2026 15:55:31 +0700 Subject: [PATCH 1/8] feat: Introduce `_resolveInitialUri` to robustly determine the initial routing URI by considering platform defaults and provided paths. --- .../zenrouter/lib/src/coordinator/base.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/zenrouter/lib/src/coordinator/base.dart b/packages/zenrouter/lib/src/coordinator/base.dart index a943ae7..c7908d9 100644 --- a/packages/zenrouter/lib/src/coordinator/base.dart +++ b/packages/zenrouter/lib/src/coordinator/base.dart @@ -236,10 +236,25 @@ abstract class Coordinator extends CoordinatorCore late final RouteInformationProvider routeInformationProvider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( - uri: initialRoutePath ?? Uri.parse('/'), + uri: _resolveInitialUri(initialRoutePath), ), ); + Uri _resolveInitialUri(Uri? initialUri) { + final defaultUri = Uri.tryParse( + WidgetsBinding.instance.platformDispatcher.defaultRouteName, + ); + if (defaultUri?.hasEmptyPath == true && initialUri != null) { + return initialUri; + } + + if (defaultUri != null && defaultUri.hasEmptyPath) { + return defaultUri.replace(path: '/'); + } + + return defaultUri ?? Uri(); + } + /// Access to the navigator state. /// /// ## Relationship From 06746431aaa1807775acff2f783e6b54c3ded16c Mon Sep 17 00:00:00 2001 From: definev Date: Fri, 27 Feb 2026 15:58:56 +0700 Subject: [PATCH 2/8] fix: Ensure default URI defaults to root path when null. --- packages/zenrouter/lib/src/coordinator/base.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zenrouter/lib/src/coordinator/base.dart b/packages/zenrouter/lib/src/coordinator/base.dart index c7908d9..9151b22 100644 --- a/packages/zenrouter/lib/src/coordinator/base.dart +++ b/packages/zenrouter/lib/src/coordinator/base.dart @@ -252,7 +252,7 @@ abstract class Coordinator extends CoordinatorCore return defaultUri.replace(path: '/'); } - return defaultUri ?? Uri(); + return defaultUri ?? Uri.parse('/'); } /// Access to the navigator state. From ada55016e822c4fd316e44a53c0c788ba7f49c61 Mon Sep 17 00:00:00 2001 From: definev Date: Fri, 27 Feb 2026 16:27:11 +0700 Subject: [PATCH 3/8] feat: Introduce `CoordinatorRouteInformationProvider` to encapsulate initial URI resolution logic, including a fix for empty path checks and streamlining its constructor. --- packages/zenrouter/CHANGELOG.md | 4 + .../zenrouter/lib/src/coordinator/base.dart | 21 +-- .../route_information_provider.dart | 34 ++++ packages/zenrouter/lib/zenrouter.dart | 1 + .../test/coordinator/restoration_test.dart | 3 +- .../route_information_provider_test.dart | 171 ++++++++++++++++++ 6 files changed, 212 insertions(+), 22 deletions(-) create mode 100644 packages/zenrouter/lib/src/coordinator/route_information_provider.dart create mode 100644 packages/zenrouter/test/coordinator/route_information_provider_test.dart diff --git a/packages/zenrouter/CHANGELOG.md b/packages/zenrouter/CHANGELOG.md index ca508bd..b0912eb 100644 --- a/packages/zenrouter/CHANGELOG.md +++ b/packages/zenrouter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 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. + ## 2.0.0 🎉 **Major Release - Core Architecture & Layouts** diff --git a/packages/zenrouter/lib/src/coordinator/base.dart b/packages/zenrouter/lib/src/coordinator/base.dart index 9151b22..5262d7f 100644 --- a/packages/zenrouter/lib/src/coordinator/base.dart +++ b/packages/zenrouter/lib/src/coordinator/base.dart @@ -234,26 +234,7 @@ abstract class Coordinator extends CoordinatorCore /// Supplies the initial URI from [initialRoutePath] or defaults to `/`. @override late final RouteInformationProvider routeInformationProvider = - PlatformRouteInformationProvider( - initialRouteInformation: RouteInformation( - uri: _resolveInitialUri(initialRoutePath), - ), - ); - - Uri _resolveInitialUri(Uri? initialUri) { - final defaultUri = Uri.tryParse( - WidgetsBinding.instance.platformDispatcher.defaultRouteName, - ); - if (defaultUri?.hasEmptyPath == true && initialUri != null) { - return initialUri; - } - - if (defaultUri != null && defaultUri.hasEmptyPath) { - return defaultUri.replace(path: '/'); - } - - return defaultUri ?? Uri.parse('/'); - } + CoordinatorRouteInformationProvider(coordinator: this); /// Access to the navigator state. /// diff --git a/packages/zenrouter/lib/src/coordinator/route_information_provider.dart b/packages/zenrouter/lib/src/coordinator/route_information_provider.dart new file mode 100644 index 0000000..2ac7b28 --- /dev/null +++ b/packages/zenrouter/lib/src/coordinator/route_information_provider.dart @@ -0,0 +1,34 @@ +import 'package:flutter/widgets.dart'; +import 'package:zenrouter/zenrouter.dart'; + +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 (defaultUri?.pathSegments.isEmpty == true && initialUri != null) { + return initialUri; + } + + if (defaultUri != null && defaultUri.hasEmptyPath) { + return defaultUri.replace(path: '/'); + } + + return defaultUri ?? Uri.parse('/'); + } +} diff --git a/packages/zenrouter/lib/zenrouter.dart b/packages/zenrouter/lib/zenrouter.dart index 2b2db3b..0374291 100644 --- a/packages/zenrouter/lib/zenrouter.dart +++ b/packages/zenrouter/lib/zenrouter.dart @@ -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'; diff --git a/packages/zenrouter/test/coordinator/restoration_test.dart b/packages/zenrouter/test/coordinator/restoration_test.dart index a14c0c3..052d042 100644 --- a/packages/zenrouter/test/coordinator/restoration_test.dart +++ b/packages/zenrouter/test/coordinator/restoration_test.dart @@ -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(); diff --git a/packages/zenrouter/test/coordinator/route_information_provider_test.dart b/packages/zenrouter/test/coordinator/route_information_provider_test.dart new file mode 100644 index 0000000..5d37b3a --- /dev/null +++ b/packages/zenrouter/test/coordinator/route_information_provider_test.dart @@ -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 get props => []; +} + +class TestCoordinator extends Coordinator { + 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()); + expect(provider, isA()); + }); + }); + + 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('/')); + }, + ); + }); +} From c86e772129c091643f61ae8ebfd9bd1f65994bf0 Mon Sep 17 00:00:00 2001 From: definev Date: Fri, 27 Feb 2026 16:51:08 +0700 Subject: [PATCH 4/8] chore: bump package version to 2.0.1 --- packages/zenrouter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zenrouter/pubspec.yaml b/packages/zenrouter/pubspec.yaml index adb083a..e3cd1b9 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.0 +version: 2.0.1 repository: https://github.com/definev/zenrouter homepage: https://github.com/definev/zenrouter/tree/main/packages/zenrouter From c32343aa391cbb21e4598492bb8e4255c1a5dfd2 Mon Sep 17 00:00:00 2001 From: Bui Dai Duong <62325868+definev@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:08:20 +0700 Subject: [PATCH 5/8] Update packages/zenrouter/lib/src/coordinator/route_information_provider.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/coordinator/route_information_provider.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/zenrouter/lib/src/coordinator/route_information_provider.dart b/packages/zenrouter/lib/src/coordinator/route_information_provider.dart index 2ac7b28..487687a 100644 --- a/packages/zenrouter/lib/src/coordinator/route_information_provider.dart +++ b/packages/zenrouter/lib/src/coordinator/route_information_provider.dart @@ -21,14 +21,21 @@ class CoordinatorRouteInformationProvider @visibleForTesting static Uri resolveInitialUri(String? platformRouteName, Uri? initialUri) { final defaultUri = Uri.tryParse(platformRouteName ?? ''); - if (defaultUri?.pathSegments.isEmpty == true && initialUri != null) { + + // 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 != null && defaultUri.hasEmptyPath) { + if (defaultUri.hasEmptyPath) { return defaultUri.replace(path: '/'); } - return defaultUri ?? Uri.parse('/'); + return defaultUri; } } From 5c1b7f8c0db624e9b0a6e9916c954c2ffd1e2730 Mon Sep 17 00:00:00 2001 From: Bui Dai Duong <62325868+definev@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:08:31 +0700 Subject: [PATCH 6/8] Update packages/zenrouter/lib/src/coordinator/route_information_provider.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../lib/src/coordinator/route_information_provider.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/zenrouter/lib/src/coordinator/route_information_provider.dart b/packages/zenrouter/lib/src/coordinator/route_information_provider.dart index 487687a..d94bc1f 100644 --- a/packages/zenrouter/lib/src/coordinator/route_information_provider.dart +++ b/packages/zenrouter/lib/src/coordinator/route_information_provider.dart @@ -1,6 +1,15 @@ import 'package:flutter/widgets.dart'; import 'package:zenrouter/zenrouter.dart'; +/// 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}) From b39b10b0b88ade036c3fe8ecc9ab932b3523db5d Mon Sep 17 00:00:00 2001 From: Bui Dai Duong <62325868+definev@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:08:40 +0700 Subject: [PATCH 7/8] Update packages/zenrouter/lib/src/coordinator/base.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/zenrouter/lib/src/coordinator/base.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/zenrouter/lib/src/coordinator/base.dart b/packages/zenrouter/lib/src/coordinator/base.dart index 5262d7f..5148b85 100644 --- a/packages/zenrouter/lib/src/coordinator/base.dart +++ b/packages/zenrouter/lib/src/coordinator/base.dart @@ -231,7 +231,9 @@ abstract class Coordinator extends CoordinatorCore /// 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 = CoordinatorRouteInformationProvider(coordinator: this); From f88d4bb873dc1c208a09d848677bc25ba855062b Mon Sep 17 00:00:00 2001 From: definev Date: Fri, 27 Feb 2026 17:17:13 +0700 Subject: [PATCH 8/8] test: Update expected initial URI to `/initial` in `RouteInformationProvider` test. --- .../test/coordinator/route_information_provider_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zenrouter/test/coordinator/route_information_provider_test.dart b/packages/zenrouter/test/coordinator/route_information_provider_test.dart index 5d37b3a..9fe2fe6 100644 --- a/packages/zenrouter/test/coordinator/route_information_provider_test.dart +++ b/packages/zenrouter/test/coordinator/route_information_provider_test.dart @@ -164,7 +164,7 @@ void main() { '::invalid::', initialUri, ); - expect(result.toString(), equals('/')); + expect(result.toString(), equals('/initial')); }, ); });