mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Feat: Add opaque, isActive, isFirst, popDisposition aspects for ModalRoute (#167324)
Feat: Add opaque, isActive, isFirst, popDisposition aspects for ModalRoute fixes: #167058 fixes: #162009 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing.
This commit is contained in:
parent
09c7f4fa5b
commit
df29894372
@ -971,6 +971,18 @@ enum _ModalRouteAspect {
|
||||
|
||||
/// Specifies the aspect corresponding to [ModalRoute.settings].
|
||||
settings,
|
||||
|
||||
/// Specifies the aspect corresponding to [ModalRoute.isActive].
|
||||
isActive,
|
||||
|
||||
/// Specifies the aspect corresponding to [ModalRoute.isFirst].
|
||||
isFirst,
|
||||
|
||||
/// Specifies the aspect corresponding to [ModalRoute.opaque].
|
||||
opaque,
|
||||
|
||||
/// Specifies the aspect corresponding to [ModalRoute.popDisposition].
|
||||
popDisposition,
|
||||
}
|
||||
|
||||
class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
|
||||
@ -979,12 +991,14 @@ class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
|
||||
required this.canPop,
|
||||
required this.impliesAppBarDismissal,
|
||||
required this.route,
|
||||
required this.opaque,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final bool isCurrent;
|
||||
final bool canPop;
|
||||
final bool impliesAppBarDismissal;
|
||||
final bool opaque;
|
||||
final Route<dynamic> route;
|
||||
|
||||
@override
|
||||
@ -992,7 +1006,8 @@ class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
|
||||
return isCurrent != old.isCurrent ||
|
||||
canPop != old.canPop ||
|
||||
impliesAppBarDismissal != old.impliesAppBarDismissal ||
|
||||
route != old.route;
|
||||
route != old.route ||
|
||||
opaque != old.opaque;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1021,6 +1036,10 @@ class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
|
||||
_ModalRouteAspect.isCurrent => isCurrent != oldWidget.isCurrent,
|
||||
_ModalRouteAspect.canPop => canPop != oldWidget.canPop,
|
||||
_ModalRouteAspect.settings => route.settings != oldWidget.route.settings,
|
||||
_ModalRouteAspect.isActive => route.isActive != oldWidget.route.isActive,
|
||||
_ModalRouteAspect.isFirst => route.isFirst != oldWidget.route.isFirst,
|
||||
_ModalRouteAspect.opaque => opaque != oldWidget.opaque,
|
||||
_ModalRouteAspect.popDisposition => route.popDisposition != oldWidget.route.popDisposition,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1143,6 +1162,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
|
||||
route: widget.route,
|
||||
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
|
||||
canPop: widget.route.canPop, // _routeSetState is called if this updates
|
||||
opaque: widget.route.opaque, // _routeSetState is called if this updates
|
||||
impliesAppBarDismissal: widget.route.impliesAppBarDismissal,
|
||||
child: Offstage(
|
||||
offstage: widget.route.offstage, // _routeSetState is called if this updates
|
||||
@ -1299,11 +1319,54 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
///
|
||||
/// 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.
|
||||
/// Calling this method creates a dependency on the [ModalRoute] associated
|
||||
/// with the given [context]. As a result, the widget corresponding to [context]
|
||||
/// will be rebuilt whenever the route's [ModalRoute.settings] changes.
|
||||
static RouteSettings? settingsOf(BuildContext context) =>
|
||||
_of(context, _ModalRouteAspect.settings)?.settings;
|
||||
|
||||
/// Returns [ModalRoute.isActive] for the modal route most closely associated
|
||||
/// with the given context.
|
||||
///
|
||||
/// Returns null if the given context is not associated with a modal route.
|
||||
///
|
||||
/// Calling this method creates a dependency on the [ModalRoute] associated
|
||||
/// with the given [context]. As a result, the widget corresponding to [context]
|
||||
/// will be rebuilt whenever the route's [ModalRoute.isActive] changes.
|
||||
static bool? isActiveOf(BuildContext context) =>
|
||||
_of(context, _ModalRouteAspect.isActive)?.isActive;
|
||||
|
||||
/// Returns [ModalRoute.isFirst] for the modal route most closely associated
|
||||
/// with the given context.
|
||||
///
|
||||
/// Returns null if the given context is not associated with a modal route.
|
||||
///
|
||||
/// Calling this method creates a dependency on the [ModalRoute] associated
|
||||
/// with the given [context]. As a result, the widget corresponding to [context]
|
||||
/// will be rebuilt whenever the route's [ModalRoute.isFirst] changes.
|
||||
static bool? isFirstOf(BuildContext context) => _of(context, _ModalRouteAspect.isFirst)?.isFirst;
|
||||
|
||||
/// Returns [ModalRoute.opaque] for the modal route most closely associated
|
||||
/// with the given context.
|
||||
///
|
||||
/// Returns null if the given context is not associated with a modal route.
|
||||
///
|
||||
/// Calling this method creates a dependency on the [ModalRoute] associated
|
||||
/// with the given [context]. As a result, the widget corresponding to [context]
|
||||
/// will be rebuilt whenever the route's [ModalRoute.opaque] changes.
|
||||
static bool? opaqueOf(BuildContext context) => _of(context, _ModalRouteAspect.opaque)?.opaque;
|
||||
|
||||
/// Returns [ModalRoute.popDisposition] for the modal route most closely associated
|
||||
/// with the given context.
|
||||
///
|
||||
/// Returns null if the given context is not associated with a modal route.
|
||||
///
|
||||
/// Calling this method creates a dependency on the [ModalRoute] associated
|
||||
/// with the given [context]. As a result, the widget corresponding to [context]
|
||||
/// will be rebuilt whenever the route's [ModalRoute.popDisposition] changes.
|
||||
static RoutePopDisposition? popDispositionOf(BuildContext context) =>
|
||||
_of(context, _ModalRouteAspect.popDisposition)?.popDisposition;
|
||||
|
||||
/// Schedule a call to [buildTransitions].
|
||||
///
|
||||
/// Whenever you need to change internal state for a [ModalRoute] object, make
|
||||
|
||||
@ -2512,6 +2512,135 @@ void main() {
|
||||
moreOrLessEquals(xLocationIntervalTwelve, epsilon: 0.1),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('ModalRoute.isFirstOf only rebuilds when first route state changes', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
int buildCount = 0;
|
||||
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
||||
|
||||
Widget buildCounter(BuildContext context) {
|
||||
buildCount++;
|
||||
final bool isFirst = ModalRoute.isFirstOf(context) ?? false;
|
||||
return Text('isFirst: $isFirst');
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(navigatorKey: navigator, home: Builder(builder: buildCounter)),
|
||||
);
|
||||
|
||||
expect(buildCount, 1);
|
||||
expect(find.text('isFirst: true'), findsOneWidget);
|
||||
|
||||
// Push a new route - first route should remain first
|
||||
navigator.currentState!.push<void>(
|
||||
MaterialPageRoute<void>(builder: (BuildContext context) => const Text('New Route')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should not rebuild because isFirst hasn't changed
|
||||
expect(buildCount, 1);
|
||||
});
|
||||
|
||||
testWidgets('ModalRoute.isActiveOf only rebuilds when route active state changes', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
int buildCount = 0;
|
||||
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
||||
|
||||
Widget buildCounter(BuildContext context) {
|
||||
buildCount++;
|
||||
final bool isActive = ModalRoute.isActiveOf(context) ?? false;
|
||||
return Text('isActive: $isActive');
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(navigatorKey: navigator, home: Builder(builder: buildCounter)),
|
||||
);
|
||||
|
||||
expect(buildCount, 1);
|
||||
expect(find.text('isActive: true'), findsOneWidget);
|
||||
|
||||
// Push a new route - first route should remain active
|
||||
navigator.currentState!.push<void>(
|
||||
MaterialPageRoute<void>(builder: (BuildContext context) => const Text('New Route')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should not rebuild because isActive hasn't changed
|
||||
expect(buildCount, 1);
|
||||
});
|
||||
|
||||
testWidgets('ModalRoute.opaqueOf only rebuilds when route opaque state changes', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
int buildCount = 0;
|
||||
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
||||
|
||||
Widget buildCounter(BuildContext context) {
|
||||
buildCount++;
|
||||
final bool isOpaque = ModalRoute.opaqueOf(context) ?? false;
|
||||
return Text('isOpaque: $isOpaque');
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(navigatorKey: navigator, home: Builder(builder: buildCounter)),
|
||||
);
|
||||
|
||||
expect(buildCount, 1);
|
||||
expect(find.text('isOpaque: true'), findsOneWidget);
|
||||
|
||||
// Push a new route - first route should remain opaque
|
||||
navigator.currentState!.push<void>(
|
||||
MaterialPageRoute<void>(builder: (BuildContext context) => const Text('New Route')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should not rebuild because isOpaque hasn't changed
|
||||
expect(buildCount, 1);
|
||||
});
|
||||
|
||||
testWidgets('ModalRoute.popDispositionOf rebuilds when PopEntry affects pop disposition', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
int buildCount = 0;
|
||||
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
||||
|
||||
Widget buildCounter(BuildContext context) {
|
||||
buildCount++;
|
||||
final RoutePopDisposition? popDisposition = ModalRoute.popDispositionOf(context);
|
||||
return Text('popDisposition: ${popDisposition?.name}');
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(navigatorKey: navigator, home: Builder(builder: buildCounter)),
|
||||
);
|
||||
|
||||
expect(buildCount, 1);
|
||||
expect(find.text('popDisposition: bubble'), findsOneWidget);
|
||||
|
||||
// Change PopScope's canPop to false
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
navigatorKey: navigator,
|
||||
home: PopScope(canPop: false, child: Builder(builder: buildCounter)),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should rebuild because popDisposition changed to doNotPop
|
||||
expect(buildCount, 2);
|
||||
expect(find.text('popDisposition: doNotPop'), findsOneWidget);
|
||||
|
||||
// Push a new route - should change from bubble to pop
|
||||
navigator.currentState!.push<void>(
|
||||
MaterialPageRoute<void>(builder: (BuildContext context) => const Text('New Route')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Shouldn't rebuild because popDisposition hasn't changed
|
||||
expect(buildCount, 2);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user