_ModalScopeStatus as InheritedModel (#149022)

According to previous discussion at https://github.com/flutter/flutter/pull/145389#discussion_r1561564845, 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.
This commit is contained in:
LinXunFeng 2024-05-30 02:46:04 +08:00 committed by GitHub
parent 2e275032d5
commit d424b64229
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 111 additions and 11 deletions

View File

@ -179,7 +179,7 @@ class BackButtonIcon extends StatelessWidget {
/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null. /// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
/// ///
/// When deciding to display a [BackButton], consider using /// 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 /// 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, /// initial route), the [BackButton] will not have any effect when pressed,
/// which could frustrate the user. /// which could frustrate the user.

View File

@ -18,6 +18,7 @@ import 'focus_manager.dart';
import 'focus_scope.dart'; import 'focus_scope.dart';
import 'focus_traversal.dart'; import 'focus_traversal.dart';
import 'framework.dart'; import 'framework.dart';
import 'inherited_model.dart';
import 'modal_barrier.dart'; import 'modal_barrier.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'overlay.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({ const _ModalScopeStatus({
required this.isCurrent, required this.isCurrent,
required this.canPop, required this.canPop,
@ -912,6 +922,15 @@ class _ModalScopeStatus extends InheritedWidget {
description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop')); description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop'));
description.add(FlagProperty('impliesAppBarDismissal', value: impliesAppBarDismissal, ifTrue: 'implies app bar dismissal')); 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<T> extends StatefulWidget { class _ModalScope<T> extends StatefulWidget {
@ -1146,10 +1165,40 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// while it is visible (specifically, if [isCurrent] or [canPop] change value). /// while it is visible (specifically, if [isCurrent] or [canPop] change value).
@optionalTypeArgs @optionalTypeArgs
static ModalRoute<T>? of<T extends Object?>(BuildContext context) { static ModalRoute<T>? of<T extends Object?>(BuildContext context) {
final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>(); return _of<T>(context);
return widget?.route as ModalRoute<T>?;
} }
static ModalRoute<T>? _of<T extends Object?>(BuildContext context, [_ModalRouteAspect? aspect]) {
return InheritedModel.inheritFrom<_ModalScopeStatus>(context, aspect: aspect)?.route as ModalRoute<T>?;
}
/// 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]. /// Schedule a call to [buildTransitions].
/// ///
/// Whenever you need to change internal state for a [ModalRoute] object, make /// Whenever you need to change internal state for a [ModalRoute] object, make

View File

@ -1357,7 +1357,7 @@ void main() {
context: scaffoldKey.currentContext!, context: scaffoldKey.currentContext!,
routeSettings: routeSettings, routeSettings: routeSettings,
builder: (BuildContext context) { builder: (BuildContext context) {
retrievedRouteSettings = ModalRoute.of(context)!.settings; retrievedRouteSettings = ModalRoute.settingsOf(context)!;
return const Text('BottomSheet'); return const Text('BottomSheet');
}, },
); );

View File

@ -1431,7 +1431,7 @@ void main() {
settings: const RouteSettings(name: 'C'), settings: const RouteSettings(name: 'C'),
builder: (BuildContext context) { builder: (BuildContext context) {
log.add('building C'); log.add('building C');
log.add('found ${ModalRoute.of(context)!.settings.name}'); log.add('found ${ModalRoute.settingsOf(context)!.name}');
return TextButton( return TextButton(
child: const Text('C'), child: const Text('C'),
onPressed: () { onPressed: () {
@ -1476,7 +1476,7 @@ void main() {
final List<String> log = <String>[]; final List<String> log = <String>[];
Route<dynamic>? nextRoute = PageRouteBuilder<int>( Route<dynamic>? nextRoute = PageRouteBuilder<int>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
log.add('building page 1 - ${ModalRoute.of(context)!.canPop}'); log.add('building page 1 - ${ModalRoute.canPopOf(context)}');
return const Placeholder(); return const Placeholder();
}, },
); );
@ -1493,32 +1493,83 @@ void main() {
expect(log, expected); expect(log, expected);
key.currentState!.pushReplacement(PageRouteBuilder<int>( key.currentState!.pushReplacement(PageRouteBuilder<int>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
log.add('building page 2 - ${ModalRoute.of(context)!.canPop}'); log.add('building page 2 - ${ModalRoute.canPopOf(context)}');
return const Placeholder(); return const Placeholder();
}, },
)); ));
expect(log, expected); expect(log, expected);
await tester.pump(); await tester.pump();
expected.add('building page 2 - false'); expected.add('building page 2 - false');
expected.add('building page 1 - false'); // page 1 is rebuilt again because isCurrent changed.
expect(log, expected); expect(log, expected);
await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150));
expect(log, expected); expect(log, expected);
key.currentState!.pushReplacement(PageRouteBuilder<int>( key.currentState!.pushReplacement(PageRouteBuilder<int>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
log.add('building page 3 - ${ModalRoute.of(context)!.canPop}'); log.add('building page 3 - ${ModalRoute.canPopOf(context)}');
return const Placeholder(); return const Placeholder();
}, },
)); ));
expect(log, expected); expect(log, expected);
await tester.pump(); await tester.pump();
expected.add('building page 3 - false'); expected.add('building page 3 - false');
expected.add('building page 2 - false'); // page 2 is rebuilt again because isCurrent changed.
expect(log, expected); expect(log, expected);
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
expect(log, expected); expect(log, expected);
}); });
testWidgets('ModelRoute can be partially depended-on', (WidgetTester tester) async {
final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
final List<String> log = <String>[];
Route<dynamic>? nextRoute = PageRouteBuilder<int>(
settings: const RouteSettings(name: 'page 1'),
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> 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<dynamic> result = nextRoute!;
nextRoute = null;
return result;
},
));
final List<String> expected = <String>['building page 1 - canPop: false'];
expect(log, expected);
key.currentState!.push(PageRouteBuilder<int>(
settings: const RouteSettings(name: 'page 2'),
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> 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<int>(
settings: const RouteSettings(name: 'page 3'),
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> 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 { testWidgets('route semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{