flutter icon indicating copy to clipboard operation
flutter copied to clipboard

setState not working as expected when Global navigatorKey added to MaterialApp

Open OutdatedGuy opened this issue 1 year ago • 6 comments
trafficstars

Steps to reproduce

  1. Run the sample code on any platform
  2. Click button on page 1
  3. See page 2
  4. Click app bar back button
  5. See page 1
  6. Click button on page 1
  7. See page 2
  8. Click button on page 2
  9. See page 3 (all other pages are now removed from routes)
  10. Comment the navigatorKey: GlobalKey<NavigatorState>() line and uncomment the navigatorKey: GlobalKeys.navigatorKey line
  11. Hot restart the app
  12. Repeat steps 2-8
  13. See that page 2 is still there with a back button
  14. Click on back button
  15. 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 avatar Oct 15 '24 14:10 OutdatedGuy

@OutdatedGuy Can you check if your case resembles https://github.com/flutter/flutter/issues/156551 or not ?

darshankawar avatar Oct 16 '24 07:10 darshankawar

@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.

OutdatedGuy avatar Oct 16 '24 07:10 OutdatedGuy

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

darshankawar avatar Oct 16 '24 12:10 darshankawar

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 avatar Oct 17 '24 08:10 TALHADWA

@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.

OutdatedGuy avatar Oct 18 '24 12:10 OutdatedGuy

Hi @OutdatedGuy, sorry for my late response!


Expected results

When GlobalKeys.navigatorKey is passed to MaterialApp.navigatorKey the route updating should work similar to as passing GlobalKey<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!

nate-thegrate avatar Oct 30 '24 00:10 nate-thegrate

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.

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?

OutdatedGuy avatar Nov 04 '24 00:11 OutdatedGuy

But that should only happen when using the key param right? Not the navigatorKey or scaffoldMessengerKey?

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, navigatorKey and scaffoldMessengerKey act 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.

nate-thegrate avatar Nov 04 '24 01:11 nate-thegrate

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.

OutdatedGuy avatar Nov 04 '24 05:11 OutdatedGuy

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.

github-actions[bot] avatar Nov 18 '24 07:11 github-actions[bot]