From 31228bb30dfc57b4602f1c7f0e648a11fb603ce4 Mon Sep 17 00:00:00 2001 From: kienhantrung Date: Thu, 26 Feb 2026 14:38:21 +0700 Subject: [PATCH 1/2] Adds support for `overrideOnExit` parameter in `TypedGoRoute` and `TypedRelativeGoRoute` annotations. --- packages/go_router_builder/CHANGELOG.md | 5 ++++ .../example/lib/on_exit_example.dart | 2 +- .../example/lib/on_exit_example.g.dart | 6 +++- .../go_router_builder/example/pubspec.yaml | 2 +- .../lib/src/route_config.dart | 30 +++++++++++++++++++ packages/go_router_builder/pubspec.yaml | 2 +- 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index f9e9da0393ab..4e08f9b9aa6a 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.3.0 + +- Adds support for `overrideOnExit` parameter in `TypedGoRoute` and `TypedRelativeGoRoute` annotations. + When set to `true`, the generated route will include `overrideOnExit: true` in the GoRoute constructor, enabling custom `onExit` callback implementation for the route. Defaults to `false`. + ## 4.2.0 - Adds supports for `TypedQueryParameter` annotation. diff --git a/packages/go_router_builder/example/lib/on_exit_example.dart b/packages/go_router_builder/example/lib/on_exit_example.dart index 155f67195354..5c5acc2d26a1 100644 --- a/packages/go_router_builder/example/lib/on_exit_example.dart +++ b/packages/go_router_builder/example/lib/on_exit_example.dart @@ -24,7 +24,7 @@ class App extends StatelessWidget { @TypedGoRoute( path: '/', routes: >[ - TypedGoRoute(path: 'sub-route'), + TypedGoRoute(path: 'sub-route', overrideOnExit: true), ], ) class HomeRoute extends GoRouteData with $HomeRoute { diff --git a/packages/go_router_builder/example/lib/on_exit_example.g.dart b/packages/go_router_builder/example/lib/on_exit_example.g.dart index e108c912ab38..9fb801fcd359 100644 --- a/packages/go_router_builder/example/lib/on_exit_example.g.dart +++ b/packages/go_router_builder/example/lib/on_exit_example.g.dart @@ -14,7 +14,11 @@ RouteBase get $homeRoute => GoRouteData.$route( path: '/', factory: $HomeRoute._fromState, routes: [ - GoRouteData.$route(path: 'sub-route', factory: $SubRoute._fromState), + GoRouteData.$route( + path: 'sub-route', + overrideOnExit: true, + factory: $SubRoute._fromState, + ), ], ); diff --git a/packages/go_router_builder/example/pubspec.yaml b/packages/go_router_builder/example/pubspec.yaml index e9bec7284d25..f46118e4098e 100644 --- a/packages/go_router_builder/example/pubspec.yaml +++ b/packages/go_router_builder/example/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: collection: ^1.15.0 flutter: sdk: flutter - go_router: ^17.1.0 + go_router: ^17.2.0 provider: 6.0.5 dev_dependencies: diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 03316a9c8440..6650e9260a1a 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -422,6 +422,7 @@ class GoRouteConfig extends RouteBaseConfig with _GoRouteMixin { required this.path, required this.name, required this.caseSensitive, + required this.overrideOnExit, required this.parentNavigatorKey, required super.routeDataClass, required super.parent, @@ -436,6 +437,15 @@ class GoRouteConfig extends RouteBaseConfig with _GoRouteMixin { /// The case sensitivity of the GoRoute to be created by this configuration. final bool caseSensitive; + /// Whether to enable the onExit callback for this route. + /// + /// When set to true, the route will include an onExit parameter in the + /// generated GoRoute constructor, allowing you to implement custom logic + /// when navigating away from this route. + /// + /// Defaults to false. + final bool overrideOnExit; + /// The parent navigator key. final String? parentNavigatorKey; @@ -505,6 +515,7 @@ mixin $_mixinName on $routeDataClassName { 'path: ${escapeDartString(path)},' '${name != null ? 'name: ${escapeDartString(name!)},' : ''}' '${caseSensitive ? '' : 'caseSensitive: $caseSensitive,'}' + '${overrideOnExit ? 'overrideOnExit: $overrideOnExit,' : ''}' '${parentNavigatorKey == null ? '' : 'parentNavigatorKey: $parentNavigatorKey,'}'; @override @@ -516,6 +527,7 @@ class RelativeGoRouteConfig extends RouteBaseConfig with _GoRouteMixin { RelativeGoRouteConfig._({ required this.path, required this.caseSensitive, + required this.overrideOnExit, required this.parentNavigatorKey, required super.routeDataClass, required super.parent, @@ -527,6 +539,15 @@ class RelativeGoRouteConfig extends RouteBaseConfig with _GoRouteMixin { /// The case sensitivity of the GoRoute to be created by this configuration. final bool caseSensitive; + /// Whether to enable the onExit callback for this route. + /// + /// When set to true, the route will include an onExit parameter in the + /// generated GoRoute constructor, allowing you to implement custom logic + /// when navigating away from this route. + /// + /// Defaults to false. + final bool overrideOnExit; + /// The parent navigator key. final String? parentNavigatorKey; @@ -582,6 +603,7 @@ mixin $_mixinName on $routeDataClassName { String get routeConstructorParameters => 'path: ${escapeDartString(path)},' '${caseSensitive ? '' : 'caseSensitive: $caseSensitive,'}' + '${overrideOnExit ? 'overrideOnExit: $overrideOnExit,' : ''}' '${parentNavigatorKey == null ? '' : 'parentNavigatorKey: $parentNavigatorKey,'}'; @override @@ -717,10 +739,14 @@ abstract class RouteBaseConfig { } final ConstantReader nameValue = reader.read('name'); final ConstantReader caseSensitiveValue = reader.read('caseSensitive'); + final ConstantReader overrideOnExitValue = reader.read( + 'overrideOnExit', + ); value = GoRouteConfig._( path: pathValue.stringValue, name: nameValue.isNull ? null : nameValue.stringValue, caseSensitive: caseSensitiveValue.boolValue, + overrideOnExit: overrideOnExitValue.boolValue, routeDataClass: classElement, parent: parent, parentNavigatorKey: _generateParameterGetterCode( @@ -744,9 +770,13 @@ abstract class RouteBaseConfig { ); } final ConstantReader caseSensitiveValue = reader.read('caseSensitive'); + final ConstantReader overrideOnExitValue = reader.read( + 'overrideOnExit', + ); value = RelativeGoRouteConfig._( path: pathValue.stringValue, caseSensitive: caseSensitiveValue.boolValue, + overrideOnExit: overrideOnExitValue.boolValue, routeDataClass: classElement, parent: parent, parentNavigatorKey: _generateParameterGetterCode( diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 9985c37166a4..d807a4382241 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 4.2.0 +version: 4.3.0 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 From 16fa126030f10d7a064e12ba28bafdf6d7695aea Mon Sep 17 00:00:00 2001 From: kienhantrung Date: Mon, 2 Mar 2026 22:57:23 +0700 Subject: [PATCH 2/2] Update test for overrideOnExit --- .../lib/not_override_on_exit_example.dart | 112 ++++++++++++++++++ .../lib/not_override_on_exit_example.g.dart | 80 +++++++++++++ .../not_override_on_exit_example_test.dart | 36 ++++++ packages/go_router_builder/pubspec.yaml | 2 +- .../test_inputs/override_on_exit.dart | 19 +++ .../test_inputs/override_on_exit.dart.expect | 56 +++++++++ 6 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 packages/go_router_builder/example/lib/not_override_on_exit_example.dart create mode 100644 packages/go_router_builder/example/lib/not_override_on_exit_example.g.dart create mode 100644 packages/go_router_builder/example/test/not_override_on_exit_example_test.dart create mode 100644 packages/go_router_builder/test_inputs/override_on_exit.dart create mode 100644 packages/go_router_builder/test_inputs/override_on_exit.dart.expect diff --git a/packages/go_router_builder/example/lib/not_override_on_exit_example.dart b/packages/go_router_builder/example/lib/not_override_on_exit_example.dart new file mode 100644 index 000000000000..dbba3c349833 --- /dev/null +++ b/packages/go_router_builder/example/lib/not_override_on_exit_example.dart @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs, unreachable_from_main + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +part 'not_override_on_exit_example.g.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({super.key}); + + @override + Widget build(BuildContext context) => + MaterialApp.router(routerConfig: _router, title: _appTitle); + + final GoRouter _router = GoRouter(routes: $appRoutes); +} + +@TypedGoRoute(path: '/') +class HomeRoute extends GoRouteData with $HomeRoute { + const HomeRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); +} + +@TypedGoRoute(path: '/sub-1-route') +class Sub1Route extends GoRouteData with $Sub1Route { + const Sub1Route(); + + @override + Widget build(BuildContext context, GoRouterState state) => const Sub1Screen(); +} + +@TypedGoRoute(path: '/sub-2-route') +class Sub2Route extends GoRouteData with $Sub2Route { + const Sub2Route(); + + @override + Widget build(BuildContext context, GoRouterState state) => const Sub2Screen(); +} + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + String? _result; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(_appTitle)), + body: Center( + child: ElevatedButton( + onPressed: () async { + final String? result = await const Sub1Route().push(context); + if (!context.mounted) { + return; + } + setState(() => _result = result); + }, + child: Text(_result ?? 'Go to sub 1 screen'), + ), + ), + ); +} + +class Sub1Screen extends StatelessWidget { + const Sub1Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('$_appTitle Sub 1 screen')), + body: Center( + child: ElevatedButton( + onPressed: () async { + final String? result = await const Sub2Route().push(context); + if (!context.mounted) { + return; + } + context.pop(result); + }, + child: const Text('Go to sub 2 screen'), + ), + ), + ); +} + +class Sub2Screen extends StatelessWidget { + const Sub2Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('$_appTitle Sub 2 screen')), + body: Center( + child: ElevatedButton( + onPressed: () => context.pop('Sub2Screen'), + child: const Text('Go back to sub 1 screen'), + ), + ), + ); +} + +const String _appTitle = 'GoRouter Example: builder'; diff --git a/packages/go_router_builder/example/lib/not_override_on_exit_example.g.dart b/packages/go_router_builder/example/lib/not_override_on_exit_example.g.dart new file mode 100644 index 000000000000..99d5920fb1ad --- /dev/null +++ b/packages/go_router_builder/example/lib/not_override_on_exit_example.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'not_override_on_exit_example.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [$homeRoute, $sub1Route, $sub2Route]; + +RouteBase get $homeRoute => + GoRouteData.$route(path: '/', factory: $HomeRoute._fromState); + +mixin $HomeRoute on GoRouteData { + static HomeRoute _fromState(GoRouterState state) => const HomeRoute(); + + @override + String get location => GoRouteData.$location('/'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +RouteBase get $sub1Route => + GoRouteData.$route(path: '/sub-1-route', factory: $Sub1Route._fromState); + +mixin $Sub1Route on GoRouteData { + static Sub1Route _fromState(GoRouterState state) => const Sub1Route(); + + @override + String get location => GoRouteData.$location('/sub-1-route'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +RouteBase get $sub2Route => + GoRouteData.$route(path: '/sub-2-route', factory: $Sub2Route._fromState); + +mixin $Sub2Route on GoRouteData { + static Sub2Route _fromState(GoRouterState state) => const Sub2Route(); + + @override + String get location => GoRouteData.$location('/sub-2-route'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} diff --git a/packages/go_router_builder/example/test/not_override_on_exit_example_test.dart b/packages/go_router_builder/example/test/not_override_on_exit_example_test.dart new file mode 100644 index 000000000000..e43b941b6a16 --- /dev/null +++ b/packages/go_router_builder/example/test/not_override_on_exit_example_test.dart @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_builder_example/not_override_on_exit_example.dart'; + +void main() { + testWidgets('HomeScreen should return result from Sub2Screen', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(App()); + expect(find.byType(HomeScreen), findsOne); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Go to sub 1 screen')); + await tester.pumpAndSettle(); + + expect(find.byType(Sub1Screen), findsOne); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Go to sub 2 screen')); + await tester.pumpAndSettle(); + + expect(find.byType(Sub2Screen), findsOne); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Go back to sub 1 screen')); + await tester.pumpAndSettle(); + + expect(find.byType(HomeScreen), findsOne); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Sub2Screen')); + + expect(find.byType(Sub1Screen), findsNothing); + expect(find.byType(Sub2Screen), findsNothing); + }); +} diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index d807a4382241..7e22992f9025 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -27,7 +27,7 @@ dev_dependencies: dart_style: ">=2.3.7 <4.0.0" flutter: sdk: flutter - go_router: ^17.1.0 + go_router: ^17.2.0 leak_tracker_flutter_testing: ">=3.0.0" package_config: ^2.1.1 pub_semver: ^2.1.5 diff --git a/packages/go_router_builder/test_inputs/override_on_exit.dart b/packages/go_router_builder/test_inputs/override_on_exit.dart new file mode 100644 index 000000000000..0f12ac41ba34 --- /dev/null +++ b/packages/go_router_builder/test_inputs/override_on_exit.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:go_router/go_router.dart'; + +mixin $OverrideOnExitRoute {} +mixin $NotOverrideOnExitRoute {} + +@TypedGoRoute( + path: '/override-on-exit-route', + overrideOnExit: true, +) +class OverrideOnExitRoute extends GoRouteData with $OverrideOnExitRoute {} + +@TypedGoRoute( + path: '/not-override-on-exit-route', +) +class NotOverrideOnExitRoute extends GoRouteData with $NotOverrideOnExitRoute {} diff --git a/packages/go_router_builder/test_inputs/override_on_exit.dart.expect b/packages/go_router_builder/test_inputs/override_on_exit.dart.expect new file mode 100644 index 000000000000..9ccda9d89406 --- /dev/null +++ b/packages/go_router_builder/test_inputs/override_on_exit.dart.expect @@ -0,0 +1,56 @@ +RouteBase get $overrideOnExitRoute => GoRouteData.$route( + path: '/override-on-exit-route', + overrideOnExit: true, + factory: $OverrideOnExitRoute._fromState, + ); + +mixin $OverrideOnExitRoute on GoRouteData { + static OverrideOnExitRoute _fromState(GoRouterState state) => + OverrideOnExitRoute(); + + @override + String get location => GoRouteData.$location( + '/override-on-exit-route', + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +RouteBase get $notOverrideOnExitRoute => GoRouteData.$route( + path: '/not-override-on-exit-route', + factory: $NotOverrideOnExitRoute._fromState, + ); + +mixin $NotOverrideOnExitRoute on GoRouteData { + static NotOverrideOnExitRoute _fromState(GoRouterState state) => + NotOverrideOnExitRoute(); + + @override + String get location => GoRouteData.$location( + '/not-override-on-exit-route', + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +}