From 381c138e8bcf59cce5e94cdeea1c31b161444690 Mon Sep 17 00:00:00 2001 From: Yashas H Majmudar Date: Sun, 30 Aug 2026 17:14:58 -0400 Subject: [PATCH 1/5] fix: check for flutter material and cupertino --- packages/go_router/lib/src/pages/cupertino.dart | 12 +++++++++++- packages/go_router/lib/src/pages/material.dart | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/go_router/lib/src/pages/cupertino.dart b/packages/go_router/lib/src/pages/cupertino.dart index b31424262ab8..dd178a8daf17 100644 --- a/packages/go_router/lib/src/pages/cupertino.dart +++ b/packages/go_router/lib/src/pages/cupertino.dart @@ -4,12 +4,22 @@ // ignore_for_file: diagnostic_describe_all_properties +// Unprefixed [CupertinoApp] is cupertino_ui's; the framework's is aliased. import 'package:cupertino_ui/cupertino_ui.dart'; +import 'package:flutter/cupertino.dart' as flutter_cupertino; + import '../misc/extensions.dart'; /// Checks for CupertinoApp in the widget tree. +/// +/// During the Material/Cupertino decoupling (flutter/flutter#184093) an app may +/// use the framework's or cupertino_ui's [CupertinoApp] — distinct types, and +/// [findAncestorWidgetOfExactType] matches only one — so both are checked to +/// install the right HeroController for shell-route Hero flights +/// (flutter/flutter#192043). Drop the framework check once it is sunset. bool isCupertinoApp(BuildContext context) => - context.findAncestorWidgetOfExactType() != null; + context.findAncestorWidgetOfExactType() != null || + context.findAncestorWidgetOfExactType() != null; /// Creates a Cupertino HeroController. HeroController createCupertinoHeroController() => CupertinoApp.createCupertinoHeroController(); diff --git a/packages/go_router/lib/src/pages/material.dart b/packages/go_router/lib/src/pages/material.dart index e5f28b667919..71c8227925cb 100644 --- a/packages/go_router/lib/src/pages/material.dart +++ b/packages/go_router/lib/src/pages/material.dart @@ -4,13 +4,22 @@ // ignore_for_file: diagnostic_describe_all_properties +// Unprefixed [MaterialApp] is material_ui's; the framework's is aliased. +import 'package:flutter/material.dart' as flutter_material; import 'package:material_ui/material_ui.dart'; import '../misc/extensions.dart'; /// Checks for MaterialApp in the widget tree. +/// +/// During the Material/Cupertino decoupling (flutter/flutter#184093) an app may +/// use the framework's or material_ui's [MaterialApp] — distinct types, and +/// [findAncestorWidgetOfExactType] matches only one — so both are checked to +/// install the right HeroController for shell-route Hero flights +/// (flutter/flutter#192043). Drop the framework check once it is sunset. bool isMaterialApp(BuildContext context) => - context.findAncestorWidgetOfExactType() != null; + context.findAncestorWidgetOfExactType() != null || + context.findAncestorWidgetOfExactType() != null; /// Creates a Material HeroController. HeroController createMaterialHeroController() => MaterialApp.createMaterialHeroController(); From 7b349c2f7a3971eeef640bac18bc2f84108b9628 Mon Sep 17 00:00:00 2001 From: Yashas H Majmudar Date: Sun, 30 Aug 2026 17:16:29 -0400 Subject: [PATCH 2/5] fix: tests --- packages/go_router/test/cupertino_test.dart | 13 ++ .../go_router/test/hero_controller_test.dart | 114 ++++++++++++++++++ packages/go_router/test/material_test.dart | 20 ++- 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 packages/go_router/test/hero_controller_test.dart diff --git a/packages/go_router/test/cupertino_test.dart b/packages/go_router/test/cupertino_test.dart index 37376f121f1c..2a43d2e9fa64 100644 --- a/packages/go_router/test/cupertino_test.dart +++ b/packages/go_router/test/cupertino_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:cupertino_ui/cupertino_ui.dart'; +import 'package:flutter/cupertino.dart' as flutter_cupertino; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/src/pages/cupertino.dart'; import 'package:material_ui/material_ui.dart'; @@ -18,6 +19,18 @@ void main() { expect(isCupertino, true); }); + testWidgets("returns [true] when Flutter's CupertinoApp is present", ( + WidgetTester tester, + ) async { + // Regression test for https://github.com/flutter/flutter/issues/192043. + // A standard Flutter app uses package:flutter/cupertino.dart's + // CupertinoApp, which is a distinct type from cupertino_ui's CupertinoApp. + final key = GlobalKey<_DummyStatefulWidgetState>(); + await tester.pumpWidget(flutter_cupertino.CupertinoApp(home: DummyStatefulWidget(key: key))); + final bool isCupertino = isCupertinoApp(key.currentContext! as Element); + expect(isCupertino, true); + }); + testWidgets('returns [false] when MaterialApp is present', (WidgetTester tester) async { final key = GlobalKey<_DummyStatefulWidgetState>(); await tester.pumpWidget(MaterialApp(home: DummyStatefulWidget(key: key))); diff --git a/packages/go_router/test/hero_controller_test.dart b/packages/go_router/test/hero_controller_test.dart new file mode 100644 index 000000000000..917b01a07155 --- /dev/null +++ b/packages/go_router/test/hero_controller_test.dart @@ -0,0 +1,114 @@ +// 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. + +// Regression tests for https://github.com/flutter/flutter/issues/192043. +// +// go_router migrated its material/cupertino helpers to the material_ui / +// cupertino_ui packages. Those packages declare their own MaterialApp / +// CupertinoApp types, distinct from the ones in package:flutter. A standard +// Flutter app is built with package:flutter's MaterialApp, so app-type +// detection based on findAncestorWidgetOfExactType stopped matching and the +// navigators go_router builds (including the nested navigators of a ShellRoute / +// StatefulShellRoute) fell back to a plain HeroController. That broke Hero +// flight animations for routes nested inside a shell when the surrounding app is +// Flutter's MaterialApp. +// +// These tests build a router under a real Flutter MaterialApp and assert that +// go_router installs its Material HeroController for every navigator it creates +// (the root navigator plus the shell's nested navigator), rather than a plain +// one. go_router's Material controller is identified by the material_ui +// MaterialRectArcTween it produces, which is distinct from the arc tween used by +// Flutter's own MaterialApp. + +import 'package:flutter/material.dart' as flutter_material; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; +import 'package:material_ui/material_ui.dart' show MaterialRectArcTween; + +// Flutter's own MaterialApp / CupertinoApp (as opposed to the leak-clean +// material_ui / cupertino_ui variants) report framework-owned objects to the +// leak tracker that are outside go_router's control, so leak tracking is +// disabled for these regression tests. +final LeakTesting _ignoreLeaks = LeakTesting.settings.withIgnoredAll(); + +/// Counts the [HeroControllerScope]s in the tree whose controller is the one +/// created by go_router's [createMaterialHeroController], identified by the +/// [material_ui] [MaterialRectArcTween] it produces. +int _goRouterMaterialControllerCount(WidgetTester tester) { + var count = 0; + for (final Element element in find.byType(HeroControllerScope).evaluate()) { + final HeroController? controller = (element.widget as HeroControllerScope).controller; + final CreateRectTween? createRectTween = controller?.createRectTween; + if (createRectTween != null && createRectTween(Rect.zero, Rect.zero) is MaterialRectArcTween) { + count++; + } + } + return count; +} + +void main() { + testWidgets('ShellRoute navigators get the Material HeroController under a Flutter MaterialApp', ( + WidgetTester tester, + ) async { + final router = GoRouter( + initialLocation: '/a', + routes: [ + ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) => child, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => const SizedBox(), + ), + ], + ), + ], + ); + addTearDown(router.dispose); + + await tester.pumpWidget(flutter_material.MaterialApp.router(routerConfig: router)); + await tester.pumpAndSettle(); + + // Both the root navigator and the shell's nested navigator must use the + // Material HeroController. Before the fix, app-type detection failed and + // these fell back to a plain HeroController, so the count would be 0. + expect(_goRouterMaterialControllerCount(tester), 2); + }, experimentalLeakTesting: _ignoreLeaks); + + testWidgets( + 'StatefulShellRoute navigators get the Material HeroController under a Flutter MaterialApp', + (WidgetTester tester) async { + final router = GoRouter( + initialLocation: '/a', + routes: [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, StatefulNavigationShell shell) => + shell, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => const SizedBox(), + ), + ], + ), + ], + ), + ], + ); + addTearDown(router.dispose); + + await tester.pumpWidget(flutter_material.MaterialApp.router(routerConfig: router)); + await tester.pumpAndSettle(); + + // The root navigator and the branch's nested navigator must both use the + // Material HeroController. Before the fix, the count would be 0. + expect(_goRouterMaterialControllerCount(tester), 2); + }, + experimentalLeakTesting: _ignoreLeaks, + ); +} diff --git a/packages/go_router/test/material_test.dart b/packages/go_router/test/material_test.dart index 131364892b98..4d855fd17884 100644 --- a/packages/go_router/test/material_test.dart +++ b/packages/go_router/test/material_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:cupertino_ui/cupertino_ui.dart'; +import 'package:flutter/material.dart' as flutter_material; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/src/pages/material.dart'; import 'package:material_ui/material_ui.dart'; @@ -11,13 +12,30 @@ import 'helpers/error_screen_helpers.dart'; void main() { group('isMaterialApp', () { - testWidgets('returns [true] when MaterialApp is present', (WidgetTester tester) async { + testWidgets('returns [true] when the material_ui MaterialApp is present', ( + WidgetTester tester, + ) async { final key = GlobalKey<_DummyStatefulWidgetState>(); await tester.pumpWidget(MaterialApp(home: DummyStatefulWidget(key: key))); final bool isMaterial = isMaterialApp(key.currentContext! as Element); expect(isMaterial, true); }); + testWidgets('returns [true] when the framework MaterialApp is present', ( + WidgetTester tester, + ) async { + // Regression test for the Material/Cupertino decoupling + // (https://github.com/flutter/flutter/issues/184093): a standard Flutter + // app uses package:flutter/material.dart's MaterialApp, which is a + // distinct type from material_ui's MaterialApp. Detection must recognize + // both so shell-route Hero flights keep working + // (https://github.com/flutter/flutter/issues/192043). + final key = GlobalKey<_DummyStatefulWidgetState>(); + await tester.pumpWidget(flutter_material.MaterialApp(home: DummyStatefulWidget(key: key))); + final bool isMaterial = isMaterialApp(key.currentContext! as Element); + expect(isMaterial, true); + }); + testWidgets('returns [false] when CupertinoApp is present', (WidgetTester tester) async { final key = GlobalKey<_DummyStatefulWidgetState>(); await tester.pumpWidget(CupertinoApp(home: DummyStatefulWidget(key: key))); From 70cb6aacca167fa486ade7f3c31bca115eedbd30 Mon Sep 17 00:00:00 2001 From: Yashas H Majmudar Date: Sun, 30 Aug 2026 17:16:42 -0400 Subject: [PATCH 3/5] chore: version bump --- AUTHORS | 1 + packages/go_router/AUTHORS | 1 + packages/go_router/CHANGELOG.md | 8 ++++++++ packages/go_router/pubspec.yaml | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 253669739d88..7826b68dfcf1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -86,3 +86,4 @@ LeanCode Piotr Denert Marcin Chudy Paweł Jakubowski +Yashas H Majmudar diff --git a/packages/go_router/AUTHORS b/packages/go_router/AUTHORS index 7174a736ef5d..eecf2b1e44e1 100644 --- a/packages/go_router/AUTHORS +++ b/packages/go_router/AUTHORS @@ -6,3 +6,4 @@ Google Inc. csells@sellsbrothers.com Hashir Shoaib +Yashas H Majmudar \ No newline at end of file diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index fee39de46aeb..7cf34f49d486 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,11 @@ +## 18.0.1 + +- Fixes Hero flight animations not playing for routes nested inside a + `ShellRoute` or `StatefulShellRoute` when the app uses Flutter's `MaterialApp` + or `CupertinoApp`. App-type detection is now package-agnostic, matching both + Flutter's `MaterialApp`/`CupertinoApp` and the `material_ui`/`cupertino_ui` + variants. + ## 18.0.0 - Migrates to material_ui and cupertino_ui. diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 2f36afd5095f..5ca622424d21 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 18.0.0 +version: 18.0.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 From b178d8443e8a59b2c0cc716a9a3a6fec97d4b3bc Mon Sep 17 00:00:00 2001 From: Yashas H Majmudar Date: Sun, 30 Aug 2026 17:27:14 -0400 Subject: [PATCH 4/5] chore: dart format --- packages/go_router/test/hero_controller_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/go_router/test/hero_controller_test.dart b/packages/go_router/test/hero_controller_test.dart index 917b01a07155..44b293faecfd 100644 --- a/packages/go_router/test/hero_controller_test.dart +++ b/packages/go_router/test/hero_controller_test.dart @@ -51,8 +51,8 @@ int _goRouterMaterialControllerCount(WidgetTester tester) { void main() { testWidgets('ShellRoute navigators get the Material HeroController under a Flutter MaterialApp', ( - WidgetTester tester, - ) async { + WidgetTester tester, + ) async { final router = GoRouter( initialLocation: '/a', routes: [ @@ -80,13 +80,13 @@ void main() { testWidgets( 'StatefulShellRoute navigators get the Material HeroController under a Flutter MaterialApp', - (WidgetTester tester) async { + (WidgetTester tester) async { final router = GoRouter( initialLocation: '/a', routes: [ StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell shell) => - shell, + shell, branches: [ StatefulShellBranch( routes: [ From 2d19f8b0420db13ff5f29ca98f58b8fc77ca2911 Mon Sep 17 00:00:00 2001 From: Yashas H Majmudar Date: Sun, 30 Aug 2026 17:50:11 -0400 Subject: [PATCH 5/5] fix: optimizations --- packages/go_router/lib/src/pages/cupertino.dart | 4 ++-- packages/go_router/lib/src/pages/material.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/go_router/lib/src/pages/cupertino.dart b/packages/go_router/lib/src/pages/cupertino.dart index dd178a8daf17..234ec8819102 100644 --- a/packages/go_router/lib/src/pages/cupertino.dart +++ b/packages/go_router/lib/src/pages/cupertino.dart @@ -18,8 +18,8 @@ import '../misc/extensions.dart'; /// install the right HeroController for shell-route Hero flights /// (flutter/flutter#192043). Drop the framework check once it is sunset. bool isCupertinoApp(BuildContext context) => - context.findAncestorWidgetOfExactType() != null || - context.findAncestorWidgetOfExactType() != null; + context.findAncestorWidgetOfExactType() != null || + context.findAncestorWidgetOfExactType() != null; /// Creates a Cupertino HeroController. HeroController createCupertinoHeroController() => CupertinoApp.createCupertinoHeroController(); diff --git a/packages/go_router/lib/src/pages/material.dart b/packages/go_router/lib/src/pages/material.dart index 71c8227925cb..ce39bef5fcad 100644 --- a/packages/go_router/lib/src/pages/material.dart +++ b/packages/go_router/lib/src/pages/material.dart @@ -18,8 +18,8 @@ import '../misc/extensions.dart'; /// install the right HeroController for shell-route Hero flights /// (flutter/flutter#192043). Drop the framework check once it is sunset. bool isMaterialApp(BuildContext context) => - context.findAncestorWidgetOfExactType() != null || - context.findAncestorWidgetOfExactType() != null; + context.findAncestorWidgetOfExactType() != null || + context.findAncestorWidgetOfExactType() != null; /// Creates a Material HeroController. HeroController createMaterialHeroController() => MaterialApp.createMaterialHeroController();