flutter
flutter copied to clipboard
setState not working as expected when Global navigatorKey added to MaterialApp
Steps to reproduce
- Run the sample code on any platform
- Click button on page 1
- See page 2
- Click app bar back button
- See page 1
- Click button on page 1
- See page 2
- Click button on page 2
- See page 3 (all other pages are now removed from routes)
- Comment the
navigatorKey: GlobalKey<NavigatorState>()line and uncomment thenavigatorKey: GlobalKeys.navigatorKeyline - Hot restart the app
- Repeat steps 2-8
- See that page 2 is still there with a back button
- Click on back button
- See page 3 was added below page 2 (i.e. it just replaced page 1 instead of the entire tree)
Expected results
When GlobalKeys.navigatorKey is passed to MaterialApp.navigatorKey the route updating should work similar to as passing GlobalKey<NavigatorState>() directly.
Actual results
When GlobalKeys.navigatorKey is used, the whole material app is not rebuilt by clearing all of the previous pages in routes.
When GlobalKey<NavigatorState>() is used, the whole material app is rebuilt removing all previously mounted pages.
Code sample
Code sample
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
return runApp(const App());
}
class GlobalKeys {
static final navigatorKey = GlobalKey<NavigatorState>();
}
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
late bool _isPage3 = false;
@override
void initState() {
super.initState();
PageState().stream.listen((isPage3) {
setState(() => _isPage3 = isPage3);
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: GlobalKey<NavigatorState>(),
// navigatorKey: GlobalKeys.navigatorKey,
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: _isPage3 ? const Page3() : const Page1(),
);
}
}
class Page1 extends StatelessWidget {
const Page1({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: <Widget>[
const Text('Page 1'),
IconButton(
icon: const Icon(Icons.next_plan),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const Page2(),
),
);
},
),
],
),
),
);
}
}
class Page2 extends StatelessWidget {
const Page2({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: <Widget>[
const Text('Page 2'),
IconButton(
icon: const Icon(Icons.change_circle),
onPressed: PageState.instance.changePage,
),
],
),
),
);
}
}
class Page3 extends StatelessWidget {
const Page3({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: <Widget>[
const Text('Page 3'),
IconButton(
icon: const Icon(Icons.change_circle),
onPressed: PageState.instance.changePage,
),
],
),
),
);
}
}
class PageState {
static final PageState instance = PageState._internal();
factory PageState() => instance;
PageState._internal();
bool _isPage3 = false;
final _controller = StreamController<bool>.broadcast();
Stream<bool> get stream => _controller.stream;
bool get isPage1 => _isPage3;
void changePage() {
_isPage3 = !_isPage3;
_controller.add(_isPage3);
}
}
Screenshots or Video
Screenshots / Video demonstration
https://github.com/user-attachments/assets/3119e7e8-a1ea-4bf3-97cb-0134ee559a28
Logs
Logs
[Paste your logs here]
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.24.3, on macOS 15.0.1 24A348 darwin-arm64, locale en-IN)
• Flutter version 3.24.3 on channel stable at /usr/development/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 2663184aa7 (5 weeks ago), 2024-09-11 16:27:48 -0500
• Engine revision 36335019a8
• Dart version 3.5.3
• DevTools version 2.37.3
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
• Android SDK at /usr/Library/Android/sdk
• Platform android-35, build-tools 35.0.0
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 16.0)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16A242d
• CocoaPods version 1.15.2
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2024.2)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
[✓] VS Code (version 1.94.2)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.98.0
[✓] VS Code (version 1.91.0-insider)
• VS Code at /Applications/Visual Studio Code - Insiders.app/Contents
• Flutter extension version 3.90.0
[✓] Connected device (1 available)
• macOS (desktop) • macos • darwin-arm64 • macOS 15.0.1 24A348 darwin-arm64
[✓] Network resources
• All expected network resources are available.
• No issues found!
@OutdatedGuy Can you check if your case resembles https://github.com/flutter/flutter/issues/156551 or not ?
@darshankawar probably not, my concern is MaterialApp not fully rebuilding the entire routes stack when a setState changes the 1st page in the routes stack.
Thanks for the update. Upon running the code sample, I do see the same behavior as reported.
Stable : 3.24.3
Master : 3.27.0-1.0.pre.10
import 'dart:async'; import 'package:flutter/material.dart';
void main() { return runApp(const App()); }
class GlobalKeys { static getnavigator(){ final navigatorKey =GlobalKey<NavigatorState>(); return navigatorKey; } }
class App extends StatefulWidget { const App({Key? key});
@override State<App> createState() => _AppState(); }
class _AppState extends State<App> { late bool _isPage3 = false;
@override void initState() {
super.initState();
PageState().stream.listen((isPage3) {
setState(() => _isPage3 = isPage3);
});
}
@override Widget build(BuildContext context) { return MaterialApp( // navigatorKey: GlobalKey<NavigatorState>(), navigatorKey: GlobalKeys.getnavigator(), theme: ThemeData( primarySwatch: Colors.indigo, ), home: _isPage3 ? const Page3() : const Page1(), ); } }
class Page1 extends StatelessWidget { const Page1({Key? key});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Center( child: Column( children: <Widget>[ const Text('Page 1'), IconButton( icon: const Icon(Icons.next_plan), onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const Page2(), ), ); }, ), ], ), ), ); } }
class Page2 extends StatelessWidget { const Page2({ Key? key});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Center( child: Column( children: <Widget>[ const Text('Page 2'), IconButton( icon: const Icon(Icons.change_circle), onPressed: PageState.instance.changePage, ), ], ), ), ); } }
class Page3 extends StatelessWidget { const Page3({Key? key});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Center( child: Column( children: <Widget>[ const Text('Page 3'), IconButton( icon: const Icon(Icons.change_circle), onPressed: PageState.instance.changePage, ), ], ), ), ); } } try this it will solve your issue.
@TALHADWA your code is returning a new GlobalKey each time it is called, it's not useful as the main purpose of having a global navigator key is to use the Navigator.of(context) without the need of context.
And your code will return a navigator key that is not attached to a material app when using it somewhere else in the project.
Hi @OutdatedGuy, sorry for my late response!
Expected results
When
GlobalKeys.navigatorKeyis passed toMaterialApp.navigatorKeythe route updating should work similar to as passingGlobalKey<NavigatorState>()directly.
I don't think this should be the expected behavior: reusing the GlobalKeys.navigatorKey instance should allow the Navigator to retain its state, whereas passing a new GlobalKey instance tells Flutter to dispose of it and inflate a new element.
As luck would have it, I made a DartPad demo for another issue that also talked about widget keys :)
In my opinion, this is working as intended, but let me know if you disagree!
I don't think this should be the expected behavior: reusing the
GlobalKeys.navigatorKeyinstance should allow the Navigator to retain its state, whereas passing a newGlobalKeyinstance tells Flutter todisposeof it and inflate a new element.
But that should only happen when using the key param right? Not the navigatorKey or scaffoldMessengerKey? Also can you tell me what is the intended behaviour for MaterialApp's route stack for:
Changing the first route/page (i.e. home param) using setState will:
a. empty the entire routes stack and then push this new page
b. just replace the first entry in route stack with this new page
As luck would have it, I made a DartPad demo for another issue that also talked about widget keys :)
Does this mean, key, navigatorKey and scaffoldMessengerKey act the same?
But that should only happen when using the
keyparam right? Not thenavigatorKeyorscaffoldMessengerKey?
Here's some pseudocode that isn't super accurate but hopefully gives a decent intuition:
class MaterialApp {
Widget build(BuildContext context) {
return ScaffoldMessenger(
key: scaffoldMessengerKey,
child: Navigator(
key: navigatorKey,
child: home,
),
);
}
}
If the MaterialApp's key changes, the entire element is thrown away and re-inflated. If navigatorKey changes, it doesn't alter the MaterialApp or ScaffoldMessenger, but the Navigator is reset.
Assuming no keys are changed, changing the home param using setState would make the MaterialApp & Navigator rebuild, but the route stack wouldn't be emptied.
Does this mean,
key,navigatorKeyandscaffoldMessengerKeyact the same?
Yes, in the sense that changing any of these keys would cause the respective widget to reset the next time it's rebuilt.
Thanks a bunch, @nate-thegrate! The pseudocode explanation really helped me understand how it works. I think that’s why passing GlobalKeys.navigatorKey or nothing (i.e. null) to navigatorKey acts the same.
I’m closing this issue as working as intended.
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.