Skip to content

Commit d424b64

Browse files
authored
_ModalScopeStatus as InheritedModel (#149022)
According to previous discussion at flutter/flutter#145389 (comment), this change makes `_ModalScopeStatus` an `InheritedModel` rather than an `InheritedWidget`, and provides the following methods. - `isCurrentOf` - `canPopOf` - `settingsOf` For example, `ModalRoute.of(context)!.settings` could become `ModalRoute.settingsOf(context)` as a performance optimization.
1 parent 2e27503 commit d424b64

File tree

4 files changed

+111
-11
lines changed

4 files changed

+111
-11
lines changed

packages/flutter/lib/src/material/action_buttons.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class BackButtonIcon extends StatelessWidget {
179179
/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
180180
///
181181
/// When deciding to display a [BackButton], consider using
182-
/// `ModalRoute.of(context)?.canPop` to check whether the current route can be
182+
/// `ModalRoute.canPopOf(context)` to check whether the current route can be
183183
/// popped. If that value is false (e.g., because the current route is the
184184
/// initial route), the [BackButton] will not have any effect when pressed,
185185
/// which could frustrate the user.

packages/flutter/lib/src/widgets/routes.dart

+52-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'focus_manager.dart';
1818
import 'focus_scope.dart';
1919
import 'focus_traversal.dart';
2020
import 'framework.dart';
21+
import 'inherited_model.dart';
2122
import 'modal_barrier.dart';
2223
import 'navigator.dart';
2324
import 'overlay.dart';
@@ -883,7 +884,16 @@ class _DismissModalAction extends DismissAction {
883884
}
884885
}
885886

886-
class _ModalScopeStatus extends InheritedWidget {
887+
enum _ModalRouteAspect {
888+
/// Specifies the aspect corresponding to [ModalRoute.isCurrent].
889+
isCurrent,
890+
/// Specifies the aspect corresponding to [ModalRoute.canPop].
891+
canPop,
892+
/// Specifies the aspect corresponding to [ModalRoute.settings].
893+
settings,
894+
}
895+
896+
class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
887897
const _ModalScopeStatus({
888898
required this.isCurrent,
889899
required this.canPop,
@@ -912,6 +922,15 @@ class _ModalScopeStatus extends InheritedWidget {
912922
description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop'));
913923
description.add(FlagProperty('impliesAppBarDismissal', value: impliesAppBarDismissal, ifTrue: 'implies app bar dismissal'));
914924
}
925+
926+
@override
927+
bool updateShouldNotifyDependent(covariant _ModalScopeStatus oldWidget, Set<_ModalRouteAspect> dependencies) {
928+
return dependencies.any((_ModalRouteAspect dependency) => switch (dependency) {
929+
_ModalRouteAspect.isCurrent => isCurrent != oldWidget.isCurrent,
930+
_ModalRouteAspect.canPop => canPop != oldWidget.canPop,
931+
_ModalRouteAspect.settings => route.settings != oldWidget.route.settings,
932+
});
933+
}
915934
}
916935

917936
class _ModalScope<T> extends StatefulWidget {
@@ -1146,10 +1165,40 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
11461165
/// while it is visible (specifically, if [isCurrent] or [canPop] change value).
11471166
@optionalTypeArgs
11481167
static ModalRoute<T>? of<T extends Object?>(BuildContext context) {
1149-
final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
1150-
return widget?.route as ModalRoute<T>?;
1168+
return _of<T>(context);
1169+
}
1170+
1171+
static ModalRoute<T>? _of<T extends Object?>(BuildContext context, [_ModalRouteAspect? aspect]) {
1172+
return InheritedModel.inheritFrom<_ModalScopeStatus>(context, aspect: aspect)?.route as ModalRoute<T>?;
11511173
}
11521174

1175+
/// Returns [ModalRoute.isCurrent] for the modal route most closely associated
1176+
/// with the given context.
1177+
///
1178+
/// Returns null if the given context is not associated with a modal route.
1179+
///
1180+
/// Use of this method will cause the given [context] to rebuild any time that
1181+
/// the [ModalRoute.isCurrent] property of the ancestor [_ModalScopeStatus] changes.
1182+
static bool? isCurrentOf(BuildContext context) => _of(context, _ModalRouteAspect.isCurrent)?.isCurrent;
1183+
1184+
/// Returns [ModalRoute.canPop] for the modal route most closely associated
1185+
/// with the given context.
1186+
///
1187+
/// Returns null if the given context is not associated with a modal route.
1188+
///
1189+
/// Use of this method will cause the given [context] to rebuild any time that
1190+
/// the [ModalRoute.canPop] property of the ancestor [_ModalScopeStatus] changes.
1191+
static bool? canPopOf(BuildContext context) => _of(context, _ModalRouteAspect.canPop)?.canPop;
1192+
1193+
/// Returns [ModalRoute.settings] for the modal route most closely associated
1194+
/// with the given context.
1195+
///
1196+
/// Returns null if the given context is not associated with a modal route.
1197+
///
1198+
/// Use of this method will cause the given [context] to rebuild any time that
1199+
/// the [ModalRoute.settings] property of the ancestor [_ModalScopeStatus] changes.
1200+
static RouteSettings? settingsOf(BuildContext context) => _of(context, _ModalRouteAspect.settings)?.settings;
1201+
11531202
/// Schedule a call to [buildTransitions].
11541203
///
11551204
/// Whenever you need to change internal state for a [ModalRoute] object, make

packages/flutter/test/material/bottom_sheet_test.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -1357,7 +1357,7 @@ void main() {
13571357
context: scaffoldKey.currentContext!,
13581358
routeSettings: routeSettings,
13591359
builder: (BuildContext context) {
1360-
retrievedRouteSettings = ModalRoute.of(context)!.settings;
1360+
retrievedRouteSettings = ModalRoute.settingsOf(context)!;
13611361
return const Text('BottomSheet');
13621362
},
13631363
);

packages/flutter/test/widgets/navigator_test.dart

+57-6
Original file line numberDiff line numberDiff line change
@@ -1431,7 +1431,7 @@ void main() {
14311431
settings: const RouteSettings(name: 'C'),
14321432
builder: (BuildContext context) {
14331433
log.add('building C');
1434-
log.add('found ${ModalRoute.of(context)!.settings.name}');
1434+
log.add('found ${ModalRoute.settingsOf(context)!.name}');
14351435
return TextButton(
14361436
child: const Text('C'),
14371437
onPressed: () {
@@ -1476,7 +1476,7 @@ void main() {
14761476
final List<String> log = <String>[];
14771477
Route<dynamic>? nextRoute = PageRouteBuilder<int>(
14781478
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1479-
log.add('building page 1 - ${ModalRoute.of(context)!.canPop}');
1479+
log.add('building page 1 - ${ModalRoute.canPopOf(context)}');
14801480
return const Placeholder();
14811481
},
14821482
);
@@ -1493,32 +1493,83 @@ void main() {
14931493
expect(log, expected);
14941494
key.currentState!.pushReplacement(PageRouteBuilder<int>(
14951495
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1496-
log.add('building page 2 - ${ModalRoute.of(context)!.canPop}');
1496+
log.add('building page 2 - ${ModalRoute.canPopOf(context)}');
14971497
return const Placeholder();
14981498
},
14991499
));
15001500
expect(log, expected);
15011501
await tester.pump();
15021502
expected.add('building page 2 - false');
1503-
expected.add('building page 1 - false'); // page 1 is rebuilt again because isCurrent changed.
15041503
expect(log, expected);
15051504
await tester.pump(const Duration(milliseconds: 150));
15061505
expect(log, expected);
15071506
key.currentState!.pushReplacement(PageRouteBuilder<int>(
15081507
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1509-
log.add('building page 3 - ${ModalRoute.of(context)!.canPop}');
1508+
log.add('building page 3 - ${ModalRoute.canPopOf(context)}');
15101509
return const Placeholder();
15111510
},
15121511
));
15131512
expect(log, expected);
15141513
await tester.pump();
15151514
expected.add('building page 3 - false');
1516-
expected.add('building page 2 - false'); // page 2 is rebuilt again because isCurrent changed.
15171515
expect(log, expected);
15181516
await tester.pump(const Duration(milliseconds: 200));
15191517
expect(log, expected);
15201518
});
15211519

1520+
testWidgets('ModelRoute can be partially depended-on', (WidgetTester tester) async {
1521+
final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
1522+
final List<String> log = <String>[];
1523+
Route<dynamic>? nextRoute = PageRouteBuilder<int>(
1524+
settings: const RouteSettings(name: 'page 1'),
1525+
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1526+
log.add('building ${ModalRoute.settingsOf(context)!.name} - canPop: ${ModalRoute.canPopOf(context)!}');
1527+
return const Placeholder();
1528+
},
1529+
);
1530+
await tester.pumpWidget(MaterialApp(
1531+
navigatorKey: key,
1532+
onGenerateRoute: (RouteSettings settings) {
1533+
assert(nextRoute != null);
1534+
final Route<dynamic> result = nextRoute!;
1535+
nextRoute = null;
1536+
return result;
1537+
},
1538+
));
1539+
final List<String> expected = <String>['building page 1 - canPop: false'];
1540+
expect(log, expected);
1541+
key.currentState!.push(PageRouteBuilder<int>(
1542+
settings: const RouteSettings(name: 'page 2'),
1543+
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1544+
log.add('building ${ModalRoute.settingsOf(context)!.name} - isCurrent: ${ModalRoute.isCurrentOf(context)!}');
1545+
return const Placeholder();
1546+
},
1547+
));
1548+
expect(log, expected);
1549+
await tester.pump();
1550+
expected.add('building page 2 - isCurrent: true');
1551+
expect(log, expected);
1552+
key.currentState!.push(PageRouteBuilder<int>(
1553+
settings: const RouteSettings(name: 'page 3'),
1554+
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1555+
log.add('building ${ModalRoute.settingsOf(context)!.name} - canPop: ${ModalRoute.canPopOf(context)!}');
1556+
return const Placeholder();
1557+
},
1558+
));
1559+
expect(log, expected);
1560+
await tester.pump();
1561+
expected.add('building page 3 - canPop: true');
1562+
expected.add('building page 2 - isCurrent: false');
1563+
expect(log, expected);
1564+
key.currentState!.pop();
1565+
await tester.pump();
1566+
expected.add('building page 2 - isCurrent: true');
1567+
expect(log, expected);
1568+
key.currentState!.pop();
1569+
await tester.pump();
1570+
expect(log, expected);
1571+
});
1572+
15221573
testWidgets('route semantics', (WidgetTester tester) async {
15231574
final SemanticsTester semantics = SemanticsTester(tester);
15241575
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{

0 commit comments

Comments
 (0)