diff --git a/packages/flutter/lib/src/material/action_buttons.dart b/packages/flutter/lib/src/material/action_buttons.dart index c417bfb2e1..9af90b6025 100644 --- a/packages/flutter/lib/src/material/action_buttons.dart +++ b/packages/flutter/lib/src/material/action_buttons.dart @@ -179,7 +179,7 @@ class BackButtonIcon extends StatelessWidget { /// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null. /// /// When deciding to display a [BackButton], consider using -/// `ModalRoute.of(context)?.canPop` to check whether the current route can be +/// `ModalRoute.canPopOf(context)` to check whether the current route can be /// popped. If that value is false (e.g., because the current route is the /// initial route), the [BackButton] will not have any effect when pressed, /// which could frustrate the user. diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 2d140c9b14..8fdb6671a9 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -18,6 +18,7 @@ import 'focus_manager.dart'; import 'focus_scope.dart'; import 'focus_traversal.dart'; import 'framework.dart'; +import 'inherited_model.dart'; import 'modal_barrier.dart'; import 'navigator.dart'; import 'overlay.dart'; @@ -883,7 +884,16 @@ class _DismissModalAction extends DismissAction { } } -class _ModalScopeStatus extends InheritedWidget { +enum _ModalRouteAspect { + /// Specifies the aspect corresponding to [ModalRoute.isCurrent]. + isCurrent, + /// Specifies the aspect corresponding to [ModalRoute.canPop]. + canPop, + /// Specifies the aspect corresponding to [ModalRoute.settings]. + settings, +} + +class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> { const _ModalScopeStatus({ required this.isCurrent, required this.canPop, @@ -912,6 +922,15 @@ class _ModalScopeStatus extends InheritedWidget { description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop')); description.add(FlagProperty('impliesAppBarDismissal', value: impliesAppBarDismissal, ifTrue: 'implies app bar dismissal')); } + + @override + bool updateShouldNotifyDependent(covariant _ModalScopeStatus oldWidget, Set<_ModalRouteAspect> dependencies) { + return dependencies.any((_ModalRouteAspect dependency) => switch (dependency) { + _ModalRouteAspect.isCurrent => isCurrent != oldWidget.isCurrent, + _ModalRouteAspect.canPop => canPop != oldWidget.canPop, + _ModalRouteAspect.settings => route.settings != oldWidget.route.settings, + }); + } } class _ModalScope extends StatefulWidget { @@ -1146,10 +1165,40 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute? of(BuildContext context) { - final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>(); - return widget?.route as ModalRoute?; + return _of(context); } + static ModalRoute? _of(BuildContext context, [_ModalRouteAspect? aspect]) { + return InheritedModel.inheritFrom<_ModalScopeStatus>(context, aspect: aspect)?.route as ModalRoute?; + } + + /// Returns [ModalRoute.isCurrent] for the modal route most closely associated + /// with the given context. + /// + /// Returns null if the given context is not associated with a modal route. + /// + /// Use of this method will cause the given [context] to rebuild any time that + /// the [ModalRoute.isCurrent] property of the ancestor [_ModalScopeStatus] changes. + static bool? isCurrentOf(BuildContext context) => _of(context, _ModalRouteAspect.isCurrent)?.isCurrent; + + /// Returns [ModalRoute.canPop] for the modal route most closely associated + /// with the given context. + /// + /// Returns null if the given context is not associated with a modal route. + /// + /// Use of this method will cause the given [context] to rebuild any time that + /// the [ModalRoute.canPop] property of the ancestor [_ModalScopeStatus] changes. + static bool? canPopOf(BuildContext context) => _of(context, _ModalRouteAspect.canPop)?.canPop; + + /// Returns [ModalRoute.settings] for the modal route most closely associated + /// with the given context. + /// + /// Returns null if the given context is not associated with a modal route. + /// + /// Use of this method will cause the given [context] to rebuild any time that + /// the [ModalRoute.settings] property of the ancestor [_ModalScopeStatus] changes. + static RouteSettings? settingsOf(BuildContext context) => _of(context, _ModalRouteAspect.settings)?.settings; + /// Schedule a call to [buildTransitions]. /// /// Whenever you need to change internal state for a [ModalRoute] object, make diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart index 8e1c78a3d6..cab5910aad 100644 --- a/packages/flutter/test/material/bottom_sheet_test.dart +++ b/packages/flutter/test/material/bottom_sheet_test.dart @@ -1357,7 +1357,7 @@ void main() { context: scaffoldKey.currentContext!, routeSettings: routeSettings, builder: (BuildContext context) { - retrievedRouteSettings = ModalRoute.of(context)!.settings; + retrievedRouteSettings = ModalRoute.settingsOf(context)!; return const Text('BottomSheet'); }, ); diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index b58ff84955..5948469a5a 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -1431,7 +1431,7 @@ void main() { settings: const RouteSettings(name: 'C'), builder: (BuildContext context) { log.add('building C'); - log.add('found ${ModalRoute.of(context)!.settings.name}'); + log.add('found ${ModalRoute.settingsOf(context)!.name}'); return TextButton( child: const Text('C'), onPressed: () { @@ -1476,7 +1476,7 @@ void main() { final List log = []; Route? nextRoute = PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { - log.add('building page 1 - ${ModalRoute.of(context)!.canPop}'); + log.add('building page 1 - ${ModalRoute.canPopOf(context)}'); return const Placeholder(); }, ); @@ -1493,32 +1493,83 @@ void main() { expect(log, expected); key.currentState!.pushReplacement(PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { - log.add('building page 2 - ${ModalRoute.of(context)!.canPop}'); + log.add('building page 2 - ${ModalRoute.canPopOf(context)}'); return const Placeholder(); }, )); expect(log, expected); await tester.pump(); expected.add('building page 2 - false'); - expected.add('building page 1 - false'); // page 1 is rebuilt again because isCurrent changed. expect(log, expected); await tester.pump(const Duration(milliseconds: 150)); expect(log, expected); key.currentState!.pushReplacement(PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { - log.add('building page 3 - ${ModalRoute.of(context)!.canPop}'); + log.add('building page 3 - ${ModalRoute.canPopOf(context)}'); return const Placeholder(); }, )); expect(log, expected); await tester.pump(); expected.add('building page 3 - false'); - expected.add('building page 2 - false'); // page 2 is rebuilt again because isCurrent changed. expect(log, expected); await tester.pump(const Duration(milliseconds: 200)); expect(log, expected); }); + testWidgets('ModelRoute can be partially depended-on', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + final List log = []; + Route? nextRoute = PageRouteBuilder( + settings: const RouteSettings(name: 'page 1'), + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + log.add('building ${ModalRoute.settingsOf(context)!.name} - canPop: ${ModalRoute.canPopOf(context)!}'); + return const Placeholder(); + }, + ); + await tester.pumpWidget(MaterialApp( + navigatorKey: key, + onGenerateRoute: (RouteSettings settings) { + assert(nextRoute != null); + final Route result = nextRoute!; + nextRoute = null; + return result; + }, + )); + final List expected = ['building page 1 - canPop: false']; + expect(log, expected); + key.currentState!.push(PageRouteBuilder( + settings: const RouteSettings(name: 'page 2'), + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + log.add('building ${ModalRoute.settingsOf(context)!.name} - isCurrent: ${ModalRoute.isCurrentOf(context)!}'); + return const Placeholder(); + }, + )); + expect(log, expected); + await tester.pump(); + expected.add('building page 2 - isCurrent: true'); + expect(log, expected); + key.currentState!.push(PageRouteBuilder( + settings: const RouteSettings(name: 'page 3'), + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + log.add('building ${ModalRoute.settingsOf(context)!.name} - canPop: ${ModalRoute.canPopOf(context)!}'); + return const Placeholder(); + }, + )); + expect(log, expected); + await tester.pump(); + expected.add('building page 3 - canPop: true'); + expected.add('building page 2 - isCurrent: false'); + expect(log, expected); + key.currentState!.pop(); + await tester.pump(); + expected.add('building page 2 - isCurrent: true'); + expect(log, expected); + key.currentState!.pop(); + await tester.pump(); + expect(log, expected); + }); + testWidgets('route semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Map routes = {