Unverified Commit 17ed4e0b authored by chunhtai's avatar chunhtai Committed by GitHub

Introduce inherited navigator observer and refactor hero controller (#58808)

parent 333eb9d7
......@@ -289,7 +289,6 @@ class _CupertinoAppState extends State<CupertinoApp> {
void initState() {
super.initState();
_heroController = CupertinoApp.createCupertinoHeroController();
_updateNavigator();
}
@override
......@@ -302,21 +301,6 @@ class _CupertinoAppState extends State<CupertinoApp> {
// Navigator has a GlobalKey).
_heroController = CupertinoApp.createCupertinoHeroController();
}
_updateNavigator();
}
List<NavigatorObserver> _navigatorObservers;
void _updateNavigator() {
if (widget.home != null ||
widget.routes.isNotEmpty ||
widget.onGenerateRoute != null ||
widget.onUnknownRoute != null) {
_navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController);
} else {
_navigatorObservers = const <NavigatorObserver>[];
}
}
// Combine the default localization for Cupertino with the ones contributed
......@@ -342,10 +326,12 @@ class _CupertinoAppState extends State<CupertinoApp> {
data: effectiveThemeData,
child: Builder(
builder: (BuildContext context) {
return WidgetsApp(
return HeroControllerScope(
controller: _heroController,
child: WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _navigatorObservers,
navigatorObservers: widget.navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
CupertinoPageRoute<T>(settings: settings, builder: builder),
home: widget.home,
......@@ -382,6 +368,8 @@ class _CupertinoAppState extends State<CupertinoApp> {
},
shortcuts: widget.shortcuts,
actions: widget.actions,
),
);
},
),
......
......@@ -571,7 +571,6 @@ class _MaterialAppState extends State<MaterialApp> {
void initState() {
super.initState();
_heroController = HeroController(createRectTween: _createRectTween);
_updateNavigator();
}
@override
......@@ -584,21 +583,6 @@ class _MaterialAppState extends State<MaterialApp> {
// Navigator has a GlobalKey).
_heroController = HeroController(createRectTween: _createRectTween);
}
_updateNavigator();
}
List<NavigatorObserver> _navigatorObservers;
void _updateNavigator() {
if (widget.home != null ||
widget.routes.isNotEmpty ||
widget.onGenerateRoute != null ||
widget.onUnknownRoute != null) {
_navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController);
} else {
_navigatorObservers = const <NavigatorObserver>[];
}
}
RectTween _createRectTween(Rect begin, Rect end) {
......@@ -619,10 +603,12 @@ class _MaterialAppState extends State<MaterialApp> {
@override
Widget build(BuildContext context) {
Widget result = WidgetsApp(
Widget result = HeroControllerScope(
controller: _heroController,
child: WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _navigatorObservers,
navigatorObservers: widget.navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
......@@ -698,6 +684,7 @@ class _MaterialAppState extends State<MaterialApp> {
},
shortcuts: widget.shortcuts,
actions: widget.actions,
),
);
assert(() {
......
......@@ -17,6 +17,7 @@ import 'binding.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'heroes.dart';
import 'overlay.dart';
import 'route_notification_messages.dart';
import 'routes.dart';
......@@ -608,6 +609,45 @@ class NavigatorObserver {
void didStopUserGesture() { }
}
/// An inherited widget to host a hero controller.
///
/// This class should not be used directly. The [MaterialApp] and [CupertinoApp]
/// use this class to host the [HeroController], and they should be the only
/// exception to use this class. If you want to subscribe your own
/// [HeroController], use the [Navigator.observers] instead.
///
/// The hosted hero controller will be picked up by the navigator in the
/// [child] subtree. Once a navigator picks up this controller, the navigator
/// will bar any navigator below its subtree from receiving this controller.
///
/// See also:
///
/// * [Navigator.observers], which is the standard way of providing a
/// [HeroController].
class HeroControllerScope extends InheritedWidget {
/// Creates a widget to host the input [controller].
const HeroControllerScope({
Key key,
this.controller,
Widget child,
}) : super(key: key, child: child);
/// The hero controller that is hosted inside this widget.
final HeroController controller;
/// Retrieves the [HeroController] from the closest [HeroControllerScope]
/// ancestor.
static HeroController of(BuildContext context) {
final HeroControllerScope host = context.dependOnInheritedWidgetOfExactType<HeroControllerScope>();
return host?.controller;
}
@override
bool updateShouldNotify(HeroControllerScope oldWidget) {
return oldWidget.controller != controller;
}
}
/// A [Route] wrapper interface that can be staged for [TransitionDelegate] to
/// decide how its underlying [Route] should transition on or off screen.
abstract class RouteTransitionRecord {
......@@ -2664,6 +2704,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends
HeroController _heroControllerFromScope;
List<NavigatorObserver> _effectiveObservers;
@override
void initState() {
super.initState();
......@@ -2675,6 +2719,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(observer.navigator == null);
observer._navigator = this;
}
_effectiveObservers = widget.observers;
// We have to manually extract the inherited widget in initState because
// the current context is not fully initialized.
final HeroControllerScope heroControllerScope = context
.getElementForInheritedWidgetOfExactType<HeroControllerScope>()
?.widget as HeroControllerScope;
_updateHeroController(heroControllerScope?.controller);
String initialRoute = widget.initialRoute;
if (widget.pages.isNotEmpty) {
_history.addAll(
......@@ -2707,6 +2760,28 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(() { _debugLocked = false; return true; }());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateHeroController(HeroControllerScope.of(context));
}
void _updateHeroController(HeroController newHeroController) {
if (_heroControllerFromScope != newHeroController) {
_heroControllerFromScope?._navigator = null;
newHeroController?._navigator = this;
_heroControllerFromScope = newHeroController;
_updateEffectiveObservers();
}
}
void _updateEffectiveObservers() {
if (_heroControllerFromScope != null)
_effectiveObservers = widget.observers + <NavigatorObserver>[_heroControllerFromScope];
else
_effectiveObservers = widget.observers;
}
@override
void didUpdateWidget(Navigator oldWidget) {
super.didUpdateWidget(oldWidget);
......@@ -2721,6 +2796,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(observer.navigator == null);
observer._navigator = this;
}
_updateEffectiveObservers();
}
if (oldWidget.pages != widget.pages) {
assert(
......@@ -2754,7 +2830,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_debugLocked = true;
return true;
}());
for (final NavigatorObserver observer in widget.observers)
for (final NavigatorObserver observer in _effectiveObservers)
observer._navigator = null;
focusScopeNode.dispose();
for (final _RouteEntry entry in _history)
......@@ -3181,19 +3257,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
}
void _flushObserverNotifications() {
if (widget.observers.isEmpty) {
if (_effectiveObservers.isEmpty) {
_observedRouteDeletions.clear();
_observedRouteAdditions.clear();
return;
}
while (_observedRouteAdditions.isNotEmpty) {
final _NavigatorObservation observation = _observedRouteAdditions.removeLast();
widget.observers.forEach(observation.notify);
_effectiveObservers.forEach(observation.notify);
}
while (_observedRouteDeletions.isNotEmpty) {
final _NavigatorObservation observation = _observedRouteDeletions.removeFirst();
widget.observers.forEach(observation.notify);
_effectiveObservers.forEach(observation.notify);
}
}
......@@ -3885,7 +3961,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_RouteEntry.willBePresentPredicate,
).route;
}
for (final NavigatorObserver observer in widget.observers)
for (final NavigatorObserver observer in _effectiveObservers)
observer.didStartUserGesture(route, previousRoute);
}
}
......@@ -3898,7 +3974,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(_userGesturesInProgress > 0);
_userGesturesInProgress -= 1;
if (_userGesturesInProgress == 0) {
for (final NavigatorObserver observer in widget.observers)
for (final NavigatorObserver observer in _effectiveObservers)
observer.didStopUserGesture();
}
}
......@@ -3933,7 +4009,11 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
Widget build(BuildContext context) {
assert(!_debugLocked);
assert(_history.isNotEmpty);
return Listener(
// Hides the HeroControllerScope for the widget subtree so that the other
// nested navigator underneath will not pick up the hero controller above
// this level.
return HeroControllerScope(
child: Listener(
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUpOrCancel,
onPointerCancel: _handlePointerUpOrCancel,
......@@ -3948,6 +4028,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
),
),
),
),
);
}
}
......@@ -150,6 +150,7 @@ void main() {
' AbsorbPointer\n'
' _PointerListener\n'
' Listener\n'
' HeroControllerScope\n'
' Navigator-[GlobalObjectKey<NavigatorState> _WidgetsAppState#00000]\n'
' IconTheme\n'
' IconTheme\n'
......@@ -182,6 +183,7 @@ void main() {
' Focus\n'
' Shortcuts\n'
' WidgetsApp-[GlobalObjectKey _MaterialAppState#00000]\n'
' HeroControllerScope\n'
' ScrollConfiguration\n'
' MaterialApp\n'
' [root]\n'
......
......@@ -1965,6 +1965,77 @@ void main() {
expect(popNextOfFirst, secondRoute);
});
testWidgets('hero controller scope works', (WidgetTester tester) async {
final GlobalKey<NavigatorState> top = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> sub = GlobalKey<NavigatorState>();
final List<NavigatorObservation> observations = <NavigatorObservation>[];
final HeroControllerSpy spy = HeroControllerSpy()
..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
observations.add(
NavigatorObservation(
current: route?.settings?.name,
previous: previousRoute?.settings?.name,
operation: 'didPush'
)
);
};
await tester.pumpWidget(
HeroControllerScope(
controller: spy,
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: top,
initialRoute: 'top1',
onGenerateRoute: (RouteSettings s) {
return MaterialPageRoute<void>(
builder: (BuildContext c) {
return Navigator(
key: sub,
initialRoute: 'sub1',
onGenerateRoute: (RouteSettings s) {
return MaterialPageRoute<void>(
builder: (BuildContext c) {
return const Placeholder();
},
settings: s,
);
},
);
},
settings: s,
);
},
),
)
)
);
// It should only observe the top navigator.
expect(observations.length, 1);
expect(observations[0].current, 'top1');
expect(observations[0].previous, isNull);
sub.currentState.push(MaterialPageRoute<void>(
settings: const RouteSettings(name:'sub2'),
builder: (BuildContext context) => const Text('sub2')
));
await tester.pumpAndSettle();
expect(find.text('sub2'), findsOneWidget);
// It should not record sub navigator.
expect(observations.length, 1);
top.currentState.push(MaterialPageRoute<void>(
settings: const RouteSettings(name:'top2'),
builder: (BuildContext context) => const Text('top2')
));
await tester.pumpAndSettle();
expect(observations.length, 2);
expect(observations[1].current, 'top2');
expect(observations[1].previous, 'top1');
});
group('Page api', (){
Widget buildNavigator({
List<Page<dynamic>> pages,
......@@ -3024,6 +3095,16 @@ class StatefulTestState extends State<StatefulTestWidget> {
}
}
class HeroControllerSpy extends HeroController {
OnObservation onPushed;
@override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPushed != null) {
onPushed(route, previousRoute);
}
}
}
class NavigatorObservation {
const NavigatorObservation({this.previous, this.current, this.operation});
final String previous;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment