_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:
parent
2e275032d5
commit
d424b64229
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -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>{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user