Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit df29894

Browse files
authored
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.
1 parent 09c7f4f commit df29894

2 files changed

Lines changed: 195 additions & 3 deletions

File tree

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

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,18 @@ enum _ModalRouteAspect {
971971

972972
/// Specifies the aspect corresponding to [ModalRoute.settings].
973973
settings,
974+
975+
/// Specifies the aspect corresponding to [ModalRoute.isActive].
976+
isActive,
977+
978+
/// Specifies the aspect corresponding to [ModalRoute.isFirst].
979+
isFirst,
980+
981+
/// Specifies the aspect corresponding to [ModalRoute.opaque].
982+
opaque,
983+
984+
/// Specifies the aspect corresponding to [ModalRoute.popDisposition].
985+
popDisposition,
974986
}
975987

976988
class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
@@ -979,20 +991,23 @@ class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
979991
required this.canPop,
980992
required this.impliesAppBarDismissal,
981993
required this.route,
994+
required this.opaque,
982995
required super.child,
983996
});
984997

985998
final bool isCurrent;
986999
final bool canPop;
9871000
final bool impliesAppBarDismissal;
1001+
final bool opaque;
9881002
final Route<dynamic> route;
9891003

9901004
@override
9911005
bool updateShouldNotify(_ModalScopeStatus old) {
9921006
return isCurrent != old.isCurrent ||
9931007
canPop != old.canPop ||
9941008
impliesAppBarDismissal != old.impliesAppBarDismissal ||
995-
route != old.route;
1009+
route != old.route ||
1010+
opaque != old.opaque;
9961011
}
9971012

9981013
@override
@@ -1021,6 +1036,10 @@ class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
10211036
_ModalRouteAspect.isCurrent => isCurrent != oldWidget.isCurrent,
10221037
_ModalRouteAspect.canPop => canPop != oldWidget.canPop,
10231038
_ModalRouteAspect.settings => route.settings != oldWidget.route.settings,
1039+
_ModalRouteAspect.isActive => route.isActive != oldWidget.route.isActive,
1040+
_ModalRouteAspect.isFirst => route.isFirst != oldWidget.route.isFirst,
1041+
_ModalRouteAspect.opaque => opaque != oldWidget.opaque,
1042+
_ModalRouteAspect.popDisposition => route.popDisposition != oldWidget.route.popDisposition,
10241043
},
10251044
);
10261045
}
@@ -1143,6 +1162,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
11431162
route: widget.route,
11441163
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
11451164
canPop: widget.route.canPop, // _routeSetState is called if this updates
1165+
opaque: widget.route.opaque, // _routeSetState is called if this updates
11461166
impliesAppBarDismissal: widget.route.impliesAppBarDismissal,
11471167
child: Offstage(
11481168
offstage: widget.route.offstage, // _routeSetState is called if this updates
@@ -1299,11 +1319,54 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
12991319
///
13001320
/// Returns null if the given context is not associated with a modal route.
13011321
///
1302-
/// Use of this method will cause the given [context] to rebuild any time that
1303-
/// the [ModalRoute.settings] property of the ancestor [_ModalScopeStatus] changes.
1322+
/// Calling this method creates a dependency on the [ModalRoute] associated
1323+
/// with the given [context]. As a result, the widget corresponding to [context]
1324+
/// will be rebuilt whenever the route's [ModalRoute.settings] changes.
13041325
static RouteSettings? settingsOf(BuildContext context) =>
13051326
_of(context, _ModalRouteAspect.settings)?.settings;
13061327

1328+
/// Returns [ModalRoute.isActive] for the modal route most closely associated
1329+
/// with the given context.
1330+
///
1331+
/// Returns null if the given context is not associated with a modal route.
1332+
///
1333+
/// Calling this method creates a dependency on the [ModalRoute] associated
1334+
/// with the given [context]. As a result, the widget corresponding to [context]
1335+
/// will be rebuilt whenever the route's [ModalRoute.isActive] changes.
1336+
static bool? isActiveOf(BuildContext context) =>
1337+
_of(context, _ModalRouteAspect.isActive)?.isActive;
1338+
1339+
/// Returns [ModalRoute.isFirst] for the modal route most closely associated
1340+
/// with the given context.
1341+
///
1342+
/// Returns null if the given context is not associated with a modal route.
1343+
///
1344+
/// Calling this method creates a dependency on the [ModalRoute] associated
1345+
/// with the given [context]. As a result, the widget corresponding to [context]
1346+
/// will be rebuilt whenever the route's [ModalRoute.isFirst] changes.
1347+
static bool? isFirstOf(BuildContext context) => _of(context, _ModalRouteAspect.isFirst)?.isFirst;
1348+
1349+
/// Returns [ModalRoute.opaque] for the modal route most closely associated
1350+
/// with the given context.
1351+
///
1352+
/// Returns null if the given context is not associated with a modal route.
1353+
///
1354+
/// Calling this method creates a dependency on the [ModalRoute] associated
1355+
/// with the given [context]. As a result, the widget corresponding to [context]
1356+
/// will be rebuilt whenever the route's [ModalRoute.opaque] changes.
1357+
static bool? opaqueOf(BuildContext context) => _of(context, _ModalRouteAspect.opaque)?.opaque;
1358+
1359+
/// Returns [ModalRoute.popDisposition] for the modal route most closely associated
1360+
/// with the given context.
1361+
///
1362+
/// Returns null if the given context is not associated with a modal route.
1363+
///
1364+
/// Calling this method creates a dependency on the [ModalRoute] associated
1365+
/// with the given [context]. As a result, the widget corresponding to [context]
1366+
/// will be rebuilt whenever the route's [ModalRoute.popDisposition] changes.
1367+
static RoutePopDisposition? popDispositionOf(BuildContext context) =>
1368+
_of(context, _ModalRouteAspect.popDisposition)?.popDisposition;
1369+
13071370
/// Schedule a call to [buildTransitions].
13081371
///
13091372
/// Whenever you need to change internal state for a [ModalRoute] object, make

packages/flutter/test/widgets/routes_test.dart

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,6 +2512,135 @@ void main() {
25122512
moreOrLessEquals(xLocationIntervalTwelve, epsilon: 0.1),
25132513
);
25142514
});
2515+
2516+
testWidgets('ModalRoute.isFirstOf only rebuilds when first route state changes', (
2517+
WidgetTester tester,
2518+
) async {
2519+
int buildCount = 0;
2520+
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
2521+
2522+
Widget buildCounter(BuildContext context) {
2523+
buildCount++;
2524+
final bool isFirst = ModalRoute.isFirstOf(context) ?? false;
2525+
return Text('isFirst: $isFirst');
2526+
}
2527+
2528+
await tester.pumpWidget(
2529+
MaterialApp(navigatorKey: navigator, home: Builder(builder: buildCounter)),
2530+
);
2531+
2532+
expect(buildCount, 1);
2533+
expect(find.text('isFirst: true'), findsOneWidget);
2534+
2535+
// Push a new route - first route should remain first
2536+
navigator.currentState!.push<void>(
2537+
MaterialPageRoute<void>(builder: (BuildContext context) => const Text('New Route')),
2538+
);
2539+
await tester.pumpAndSettle();
2540+
2541+
// Should not rebuild because isFirst hasn't changed
2542+
expect(buildCount, 1);
2543+
});
2544+
2545+
testWidgets('ModalRoute.isActiveOf only rebuilds when route active state changes', (
2546+
WidgetTester tester,
2547+
) async {
2548+
int buildCount = 0;
2549+
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
2550+
2551+
Widget buildCounter(BuildContext context) {
2552+
buildCount++;
2553+
final bool isActive = ModalRoute.isActiveOf(context) ?? false;
2554+
return Text('isActive: $isActive');
2555+
}
2556+
2557+
await tester.pumpWidget(
2558+
MaterialApp(navigatorKey: navigator, home: Builder(builder: buildCounter)),
2559+
);
2560+
2561+
expect(buildCount, 1);
2562+
expect(find.text('isActive: true'), findsOneWidget);
2563+
2564+
// Push a new route - first route should remain active
2565+
navigator.currentState!.push<void>(
2566+
MaterialPageRoute<void>(builder: (BuildContext context) => const Text('New Route')),
2567+
);
2568+
await tester.pumpAndSettle();
2569+
2570+
// Should not rebuild because isActive hasn't changed
2571+
expect(buildCount, 1);
2572+
});
2573+
2574+
testWidgets('ModalRoute.opaqueOf only rebuilds when route opaque state changes', (
2575+
WidgetTester tester,
2576+
) async {
2577+
int buildCount = 0;
2578+
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
2579+
2580+
Widget buildCounter(BuildContext context) {
2581+
buildCount++;
2582+
final bool isOpaque = ModalRoute.opaqueOf(context) ?? false;
2583+
return Text('isOpaque: $isOpaque');
2584+
}
2585+
2586+
await tester.pumpWidget(
2587+
MaterialApp(navigatorKey: navigator, home: Builder(builder: buildCounter)),
2588+
);
2589+
2590+
expect(buildCount, 1);
2591+
expect(find.text('isOpaque: true'), findsOneWidget);
2592+
2593+
// Push a new route - first route should remain opaque
2594+
navigator.currentState!.push<void>(
2595+
MaterialPageRoute<void>(builder: (BuildContext context) => const Text('New Route')),
2596+
);
2597+
await tester.pumpAndSettle();
2598+
2599+
// Should not rebuild because isOpaque hasn't changed
2600+
expect(buildCount, 1);
2601+
});
2602+
2603+
testWidgets('ModalRoute.popDispositionOf rebuilds when PopEntry affects pop disposition', (
2604+
WidgetTester tester,
2605+
) async {
2606+
int buildCount = 0;
2607+
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
2608+
2609+
Widget buildCounter(BuildContext context) {
2610+
buildCount++;
2611+
final RoutePopDisposition? popDisposition = ModalRoute.popDispositionOf(context);
2612+
return Text('popDisposition: ${popDisposition?.name}');
2613+
}
2614+
2615+
await tester.pumpWidget(
2616+
MaterialApp(navigatorKey: navigator, home: Builder(builder: buildCounter)),
2617+
);
2618+
2619+
expect(buildCount, 1);
2620+
expect(find.text('popDisposition: bubble'), findsOneWidget);
2621+
2622+
// Change PopScope's canPop to false
2623+
await tester.pumpWidget(
2624+
MaterialApp(
2625+
navigatorKey: navigator,
2626+
home: PopScope(canPop: false, child: Builder(builder: buildCounter)),
2627+
),
2628+
);
2629+
await tester.pumpAndSettle();
2630+
2631+
// Should rebuild because popDisposition changed to doNotPop
2632+
expect(buildCount, 2);
2633+
expect(find.text('popDisposition: doNotPop'), findsOneWidget);
2634+
2635+
// Push a new route - should change from bubble to pop
2636+
navigator.currentState!.push<void>(
2637+
MaterialPageRoute<void>(builder: (BuildContext context) => const Text('New Route')),
2638+
);
2639+
await tester.pumpAndSettle();
2640+
2641+
// Shouldn't rebuild because popDisposition hasn't changed
2642+
expect(buildCount, 2);
2643+
});
25152644
});
25162645

25172646
testWidgets('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async {

0 commit comments

Comments
 (0)