Commit b339c715 authored by weisong0's avatar weisong0 Committed by Adam Barth

Allow multiple observers for the Navigator and MaterialApp (#7883)

* allow mulitple observers for Navigator and MaterialApp

* add test for the navigator observers

* fix style

* add test for adding/removing a navigator observer
parent ff14f35d
...@@ -57,6 +57,7 @@ class MaterialApp extends StatefulWidget { ...@@ -57,6 +57,7 @@ class MaterialApp extends StatefulWidget {
this.initialRoute, this.initialRoute,
this.onGenerateRoute, this.onGenerateRoute,
this.onLocaleChanged, this.onLocaleChanged,
this.navigatorObservers: const <NavigatorObserver>[],
this.debugShowMaterialGrid: false, this.debugShowMaterialGrid: false,
this.showPerformanceOverlay: false, this.showPerformanceOverlay: false,
this.checkerboardRasterCacheImages: false, this.checkerboardRasterCacheImages: false,
...@@ -154,6 +155,9 @@ class MaterialApp extends StatefulWidget { ...@@ -154,6 +155,9 @@ class MaterialApp extends StatefulWidget {
/// representative of what will happen in release mode. /// representative of what will happen in release mode.
final bool debugShowCheckedModeBanner; final bool debugShowCheckedModeBanner;
/// The list of observers for the [Navigator] created for this app.
final List<NavigatorObserver> navigatorObservers;
/// Turns on a [GridPaper] overlay that paints a baseline grid /// Turns on a [GridPaper] overlay that paints a baseline grid
/// Material apps: /// Material apps:
/// https://material.google.com/layout/metrics-keylines.html /// https://material.google.com/layout/metrics-keylines.html
...@@ -278,7 +282,9 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -278,7 +282,9 @@ class _MaterialAppState extends State<MaterialApp> {
textStyle: _errorTextStyle, textStyle: _errorTextStyle,
// blue[500] is the primary color of the default theme // blue[500] is the primary color of the default theme
color: config.color ?? theme?.primaryColor ?? Colors.blue[500], color: config.color ?? theme?.primaryColor ?? Colors.blue[500],
navigatorObserver: _heroController, navigatorObservers:
new List<NavigatorObserver>.from(config.navigatorObservers)
..add(_heroController),
initialRoute: config.initialRoute, initialRoute: config.initialRoute,
onGenerateRoute: _onGenerateRoute, onGenerateRoute: _onGenerateRoute,
onLocaleChanged: config.onLocaleChanged, onLocaleChanged: config.onLocaleChanged,
......
...@@ -44,7 +44,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -44,7 +44,7 @@ class WidgetsApp extends StatefulWidget {
this.title, this.title,
this.textStyle, this.textStyle,
@required this.color, @required this.color,
this.navigatorObserver, this.navigatorObservers: const <NavigatorObserver>[],
this.initialRoute, this.initialRoute,
this.onLocaleChanged, this.onLocaleChanged,
this.showPerformanceOverlay: false, this.showPerformanceOverlay: false,
...@@ -111,8 +111,8 @@ class WidgetsApp extends StatefulWidget { ...@@ -111,8 +111,8 @@ class WidgetsApp extends StatefulWidget {
/// representative of what will happen in release mode. /// representative of what will happen in release mode.
final bool debugShowCheckedModeBanner; final bool debugShowCheckedModeBanner;
/// The observer for the Navigator created for this app. /// The list of observers for the [Navigator] created for this app.
final NavigatorObserver navigatorObserver; final List<NavigatorObserver> navigatorObservers;
/// If true, forces the performance overlay to be visible in all instances. /// If true, forces the performance overlay to be visible in all instances.
/// ///
...@@ -203,7 +203,7 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -203,7 +203,7 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
key: _navigator, key: _navigator,
initialRoute: config.initialRoute ?? ui.window.defaultRouteName, initialRoute: config.initialRoute ?? ui.window.defaultRouteName,
onGenerateRoute: config.onGenerateRoute, onGenerateRoute: config.onGenerateRoute,
observer: config.navigatorObserver observers: config.navigatorObservers
) )
) )
) )
......
...@@ -445,7 +445,7 @@ class Navigator extends StatefulWidget { ...@@ -445,7 +445,7 @@ class Navigator extends StatefulWidget {
this.initialRoute, this.initialRoute,
@required this.onGenerateRoute, @required this.onGenerateRoute,
this.onUnknownRoute, this.onUnknownRoute,
this.observer this.observers: const <NavigatorObserver>[]
}) : super(key: key) { }) : super(key: key) {
assert(onGenerateRoute != null); assert(onGenerateRoute != null);
} }
...@@ -466,8 +466,8 @@ class Navigator extends StatefulWidget { ...@@ -466,8 +466,8 @@ class Navigator extends StatefulWidget {
/// requests to push routes, such as from Android intents. /// requests to push routes, such as from Android intents.
final RouteFactory onUnknownRoute; final RouteFactory onUnknownRoute;
/// An observer for this navigator. /// A list of observers for this navigator.
final NavigatorObserver observer; final List<NavigatorObserver> observers;
/// The default name for the initial route. /// The default name for the initial route.
static const String defaultRouteName = '/'; static const String defaultRouteName = '/';
...@@ -668,8 +668,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -668,8 +668,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
assert(config.observer == null || config.observer.navigator == null); for (NavigatorObserver observer in config.observers) {
config.observer?._navigator = this; assert(observer.navigator == null);
observer._navigator = this;
}
push(config.onGenerateRoute(new RouteSettings( push(config.onGenerateRoute(new RouteSettings(
name: config.initialRoute ?? Navigator.defaultRouteName, name: config.initialRoute ?? Navigator.defaultRouteName,
isInitialRoute: true isInitialRoute: true
...@@ -678,10 +680,13 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -678,10 +680,13 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
@override @override
void didUpdateConfig(Navigator oldConfig) { void didUpdateConfig(Navigator oldConfig) {
if (oldConfig.observer != config.observer) { if (oldConfig.observers != config.observers) {
oldConfig.observer?._navigator = null; for (NavigatorObserver observer in oldConfig.observers)
assert(config.observer == null || config.observer.navigator == null); observer._navigator = null;
config.observer?._navigator = this; for (NavigatorObserver observer in config.observers) {
assert(observer.navigator == null);
observer._navigator = this;
}
} }
} }
...@@ -689,7 +694,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -689,7 +694,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
void dispose() { void dispose() {
assert(!_debugLocked); assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }); assert(() { _debugLocked = true; return true; });
config.observer?._navigator = null; for (NavigatorObserver observer in config.observers)
observer._navigator = null;
final List<Route<dynamic>> doomed = _poppedRoutes.toList()..addAll(_history); final List<Route<dynamic>> doomed = _poppedRoutes.toList()..addAll(_history);
for (Route<dynamic> route in doomed) for (Route<dynamic> route in doomed)
route.dispose(); route.dispose();
...@@ -768,7 +774,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -768,7 +774,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
route.didChangeNext(null); route.didChangeNext(null);
if (oldRoute != null) if (oldRoute != null)
oldRoute.didChangeNext(route); oldRoute.didChangeNext(route);
config.observer?.didPush(route, oldRoute); for (NavigatorObserver observer in config.observers)
observer.didPush(route, oldRoute);
}); });
assert(() { _debugLocked = false; return true; }); assert(() { _debugLocked = false; return true; });
_cancelActivePointers(); _cancelActivePointers();
...@@ -850,7 +857,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -850,7 +857,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
newRoute.didChangeNext(null); newRoute.didChangeNext(null);
if (index > 0) if (index > 0)
_history[index - 1].didChangeNext(newRoute); _history[index - 1].didChangeNext(newRoute);
config.observer?.didPush(newRoute, oldRoute); for (NavigatorObserver observer in config.observers)
observer.didPush(newRoute, oldRoute);
}); });
assert(() { _debugLocked = false; return true; }); assert(() { _debugLocked = false; return true; });
_cancelActivePointers(); _cancelActivePointers();
...@@ -957,7 +965,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -957,7 +965,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
if (route._navigator != null) if (route._navigator != null)
_poppedRoutes.add(route); _poppedRoutes.add(route);
_history.last.didPopNext(route); _history.last.didPopNext(route);
config.observer?.didPop(route, _history.last); for (NavigatorObserver observer in config.observers)
observer.didPop(route, _history.last);
}); });
} else { } else {
assert(() { _debugLocked = false; return true; }); assert(() { _debugLocked = false; return true; });
...@@ -1026,13 +1035,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1026,13 +1035,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// Used for the iOS back gesture. /// Used for the iOS back gesture.
void didStartUserGesture() { void didStartUserGesture() {
_userGestureInProgress = true; _userGestureInProgress = true;
config.observer?.didStartUserGesture(); for (NavigatorObserver observer in config.observers)
observer.didStartUserGesture();
} }
/// A user gesture is no longer controlling the navigator. /// A user gesture is no longer controlling the navigator.
void didStopUserGesture() { void didStopUserGesture() {
_userGestureInProgress = false; _userGestureInProgress = false;
config.observer?.didStopUserGesture(); for (NavigatorObserver observer in config.observers)
observer.didStopUserGesture();
} }
final Set<int> _activePointers = new Set<int>(); final Set<int> _activePointers = new Set<int>();
......
...@@ -89,6 +89,28 @@ class OnTapPage extends StatelessWidget { ...@@ -89,6 +89,28 @@ class OnTapPage extends StatelessWidget {
} }
} }
typedef void OnPushed(Route<dynamic> route, Route<dynamic> previousRoute);
typedef void OnPopped(Route<dynamic> route, Route<dynamic> previousRoute);
class TestObserver extends NavigatorObserver {
OnPushed onPushed;
OnPopped onPopped;
@override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPushed != null) {
onPushed(route, previousRoute);
}
}
@override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPopped != null) {
onPopped(route, previousRoute);
}
}
}
void main() { void main() {
testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async { testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
...@@ -260,6 +282,114 @@ void main() { ...@@ -260,6 +282,114 @@ void main() {
expect(find.text('B'), findsOneWidget); expect(find.text('B'), findsOneWidget);
}); });
testWidgets('Push and pop should trigger the observers',
(WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }),
'/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }),
};
bool isPushed = false;
bool isPopped = false;
TestObserver observer = new TestObserver()
..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
// Pushes the initial route.
expect(route is PageRoute && route.settings.name == '/', isTrue);
expect(previousRoute, isNull);
isPushed = true;
}
..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) {
isPopped = true;
};
await tester.pumpWidget(new MaterialApp(
routes: routes,
navigatorObservers: <NavigatorObserver>[observer],
));
expect(find.text('/'), findsOneWidget);
expect(find.text('A'), findsNothing);
expect(isPushed, isTrue);
expect(isPopped, isFalse);
isPushed = false;
isPopped = false;
observer.onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
expect(route is PageRoute && route.settings.name == '/A', isTrue);
expect(previousRoute is PageRoute && previousRoute.settings.name == '/', isTrue);
isPushed = true;
};
await tester.tap(find.text('/'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('/'), findsNothing);
expect(find.text('A'), findsOneWidget);
expect(isPushed, isTrue);
expect(isPopped, isFalse);
isPushed = false;
isPopped = false;
observer.onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) {
expect(route is PageRoute && route.settings.name == '/A', isTrue);
expect(previousRoute is PageRoute && previousRoute.settings.name == '/', isTrue);
isPopped = true;
};
await tester.tap(find.text('A'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('/'), findsOneWidget);
expect(find.text('A'), findsNothing);
expect(isPushed, isFalse);
expect(isPopped, isTrue);
});
testWidgets("Add and remove an observer should work", (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }),
'/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }),
};
bool isPushed = false;
bool isPopped = false;
TestObserver observer1 = new TestObserver();
TestObserver observer2 = new TestObserver()
..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
isPushed = true;
}
..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) {
isPopped = true;
};
await tester.pumpWidget(new MaterialApp(
routes: routes,
navigatorObservers: <NavigatorObserver>[observer1],
));
expect(isPushed, isFalse);
expect(isPopped, isFalse);
await tester.pumpWidget(new MaterialApp(
routes: routes,
navigatorObservers: <NavigatorObserver>[observer1, observer2],
));
await tester.tap(find.text('/'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(isPushed, isTrue);
expect(isPopped, isFalse);
isPushed = false;
isPopped = false;
await tester.pumpWidget(new MaterialApp(
routes: routes,
navigatorObservers: <NavigatorObserver>[observer1],
));
await tester.tap(find.text('A'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(isPushed, isFalse);
expect(isPopped, isFalse);
});
testWidgets('replaceNamed', (WidgetTester tester) async { testWidgets('replaceNamed', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { Navigator.pushReplacementNamed(context, '/A'); }), '/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { Navigator.pushReplacementNamed(context, '/A'); }),
......
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