Skip to content

Commit

Permalink
_ModalScopeStatus as InheritedModel (#149022)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
LinXunFeng authored May 29, 2024
1 parent 2e27503 commit d424b64
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 11 deletions.
2 changes: 1 addition & 1 deletion packages/flutter/lib/src/material/action_buttons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
55 changes: 52 additions & 3 deletions packages/flutter/lib/src/widgets/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<T> extends StatefulWidget {
Expand Down Expand Up @@ -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).
@optionalTypeArgs
static ModalRoute<T>? of<T extends Object?>(BuildContext context) {
final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
return widget?.route as ModalRoute<T>?;
return _of<T>(context);
}

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].
///
/// Whenever you need to change internal state for a [ModalRoute] object, make
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter/test/material/bottom_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
);
Expand Down
63 changes: 57 additions & 6 deletions packages/flutter/test/widgets/navigator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: () {
Expand Down Expand Up @@ -1476,7 +1476,7 @@ void main() {
final List<String> log = <String>[];
Route<dynamic>? nextRoute = PageRouteBuilder<int>(
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();
},
);
Expand All @@ -1493,32 +1493,83 @@ void main() {
expect(log, expected);
key.currentState!.pushReplacement(PageRouteBuilder<int>(
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();
},
));
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<int>(
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();
},
));
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<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 {
final SemanticsTester semantics = SemanticsTester(tester);
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
Expand Down

0 comments on commit d424b64

Please sign in to comment.