diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 2b118f4e69b..fb32cb00c24 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -79,6 +79,7 @@ class MaterialApp extends StatefulWidget { /// The boolean arguments, [routes], and [navigatorObservers], must not be null. MaterialApp({ // can't be const because the asserts use methods on Map :-( Key key, + this.navigatorKey, this.title: '', this.onGenerateTitle, this.color, @@ -128,6 +129,19 @@ class MaterialApp extends StatefulWidget { ), super(key: key); + /// A key to use when building the [Navigator]. + /// + /// If a [navigatorKey] is specified, the [Navigator] can be directly + /// manipulated without first obtaining it from a [BuildContext] via + /// [Navigator.of]: from the [navigatorKey], use the [GlobalKey.currentState] + /// getter. + /// + /// If this is changed, a new [Navigator] will be created, losing all the + /// application state in the process; in that case, the [navigatorObservers] + /// must also be changed, since the previous observers will be attached to the + /// previous navigator. + final GlobalKey navigatorKey; + /// A one-line description used by the device to identify the app for the user. /// /// On Android the titles appear above the task manager's app snapshots which are @@ -403,6 +417,9 @@ class MaterialApp extends StatefulWidget { final bool debugShowCheckedModeBanner; /// The list of observers for the [Navigator] created for this app. + /// + /// This list must be replaced by a list of newly-created observers if the + /// [navigatorKey] is changed. final List navigatorObservers; /// Turns on a [GridPaper] overlay that paints a baseline grid @@ -453,6 +470,18 @@ class _MaterialAppState extends State { _heroController = new HeroController(createRectTween: _createRectTween); } + @override + void didUpdateWidget(MaterialApp oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.navigatorKey != oldWidget.navigatorKey) { + // If the Navigator changes, we have to create a new observer, because the + // old Navigator won't be disposed (and thus won't unregister with its + // observers) until after the new one has been created (because the + // Navigator has a GlobalKey). + _heroController = new HeroController(createRectTween: _createRectTween); + } + } + // Combine the Localizations for Material with the ones contributed // by the localizationsDelegates parameter, if any. Only the first delegate // of a particular LocalizationsDelegate.type is loaded so the @@ -526,6 +555,7 @@ class _MaterialAppState extends State { isMaterialAppTheme: true, child: new WidgetsApp( key: new GlobalObjectKey(this), + navigatorKey: widget.navigatorKey, title: widget.title, onGenerateTitle: widget.onGenerateTitle, textStyle: _errorTextStyle, diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 9ddedd63126..4b4ab884de0 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -67,6 +67,7 @@ class WidgetsApp extends StatefulWidget { /// By default supportedLocales is `[const Locale('en', 'US')]`. WidgetsApp({ // can't be const because the asserts use methods on Iterable :-( Key key, + this.navigatorKey, @required this.onGenerateRoute, this.onUnknownRoute, this.title: '', @@ -99,6 +100,19 @@ class WidgetsApp extends StatefulWidget { assert(debugShowWidgetInspector != null), super(key: key); + /// A key to use when building the [Navigator]. + /// + /// If a [navigatorKey] is specified, the [Navigator] can be directly + /// manipulated without first obtaining it from a [BuildContext] via + /// [Navigator.of]: from the [navigatorKey], use the [GlobalKey.currentState] + /// getter. + /// + /// If this is changed, a new [Navigator] will be created, losing all the + /// application state in the process; in that case, the [navigatorObservers] + /// must also be changed, since the previous observers will be attached to the + /// previous navigator. + final GlobalKey navigatorKey; + /// A one-line description used by the device to identify the app for the user. /// /// On Android the titles appear above the task manager's app snapshots which are @@ -285,6 +299,9 @@ class WidgetsApp extends StatefulWidget { final bool debugShowCheckedModeBanner; /// The list of observers for the [Navigator] created for this app. + /// + /// This list must be replaced by a list of newly-created observers if the + /// [navigatorKey] is changed. final List navigatorObservers; /// If true, forces the performance overlay to be visible in all instances. @@ -315,7 +332,7 @@ class WidgetsApp extends StatefulWidget { } class _WidgetsAppState extends State implements WidgetsBindingObserver { - GlobalObjectKey _navigator; + GlobalKey _navigator; Locale _locale; Locale _resolveLocale(Locale newLocale, Iterable supportedLocales) { @@ -338,11 +355,22 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv @override void initState() { super.initState(); - _navigator = new GlobalObjectKey(this); + _updateNavigator(); _locale = _resolveLocale(ui.window.locale, widget.supportedLocales); WidgetsBinding.instance.addObserver(this); } + @override + void didUpdateWidget(WidgetsApp oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.navigatorKey != oldWidget.navigatorKey) + _updateNavigator(); + } + + void _updateNavigator() { + _navigator = widget.navigatorKey ?? new GlobalObjectKey(this); + } + @override void dispose() { WidgetsBinding.instance.removeObserver(this); diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index f17dd548921..e9387ba98d6 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -317,4 +317,25 @@ void main() { expect(textScaleFactor, isNotNull); expect(textScaleFactor, equals(1.0)); }); + + testWidgets('MaterialApp.navigatorKey', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + await tester.pumpWidget(new MaterialApp( + navigatorKey: key, + color: const Color(0xFF112233), + home: const Placeholder(), + )); + expect(key.currentState, const isInstanceOf()); + await tester.pumpWidget(new MaterialApp( + color: const Color(0xFF112233), + home: const Placeholder(), + )); + expect(key.currentState, isNull); + await tester.pumpWidget(new MaterialApp( + navigatorKey: key, + color: const Color(0xFF112233), + home: const Placeholder(), + )); + expect(key.currentState, const isInstanceOf()); + }); } diff --git a/packages/flutter/test/widgets/app_navigator_key_test.dart b/packages/flutter/test/widgets/app_navigator_key_test.dart new file mode 100644 index 00000000000..93bea3661ef --- /dev/null +++ b/packages/flutter/test/widgets/app_navigator_key_test.dart @@ -0,0 +1,36 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +final RouteFactory generateRoute = (RouteSettings settings) => new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation1, Animation animation2) { + return const Placeholder(); + }, +); + +void main() { + testWidgets('WidgetsApp.navigatorKey', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + await tester.pumpWidget(new WidgetsApp( + navigatorKey: key, + color: const Color(0xFF112233), + onGenerateRoute: generateRoute, + )); + expect(key.currentState, const isInstanceOf()); + await tester.pumpWidget(new WidgetsApp( + color: const Color(0xFF112233), + onGenerateRoute: generateRoute, + )); + expect(key.currentState, isNull); + await tester.pumpWidget(new WidgetsApp( + navigatorKey: key, + color: const Color(0xFF112233), + onGenerateRoute: generateRoute, + )); + expect(key.currentState, const isInstanceOf()); + }); +}