Skip to content

DialogRoute doesn't rebuild unless the key changes. #115639

@ValentinVignal

Description

@ValentinVignal

Steps to Reproduce

  1. Execute flutter run on the code sample (see "Code sample" section below)
  2. Click on "Dialog" button to display the dialog
  3. Try to go to the dialog on the right
  4. Notice that the dialog doesn't change

Expected results:

I'm expecting the dialog to rebuild

Actual results:

It is not rebuilding.
I can force it to rebuild by changing the key ValueKey('dialog-$_selected'), instead of ValueKey('dialog'),
But doing so prevents me from reusing states.

You can see in the logs that it goes through the initState of _DialogWidgetState:

flutter: initState 1
flutter: initState 2
flutter: initState 3
flutter: initState 2
flutter: initState 1

I believe this might be coming from

// We cache the part of the modal scope that doesn't change from frame to
// frame so that we minimize the amount of building that happens.
Widget? _modalScopeCache;
// one of the builders
Widget _buildModalScope(BuildContext context) {
// To be sorted before the _modalBarrier.
return _modalScopeCache ??= Semantics(

and/or

// We cache the result of calling the route's buildPage, and clear the cache
// whenever the dependencies change. This implements the contract described in
// the documentation for buildPage, namely that it gets called once, unless
// something like a ModalRoute.of() dependency triggers an update.
Widget? _page;

As a comparison, I also included another example with the MaterialPage which works fine.

Code sample

Or you can check out https://github.com/ValentinVignal/flutter_app_stable/tree/flutter/dialog-being-cached

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class AppRoute {
  const AppRoute(this.location);
  final int? location;
}

class AppRouter extends RouterDelegate<AppRoute>
    with PopNavigatorRouterDelegateMixin, ChangeNotifier {
  // get correct state of router
  @override
  AppRoute get currentConfiguration => AppRoute(_selected);

  int? _selected;
  int get selected => _selected!;
  set selected(int value) {
    _selected = value == 0 ? null : value;
    notifyListeners();
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: _navigatorKey,
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }
        _selected = null;
        notifyListeners();
        return true;
      },
      pages: [
        const MaterialPage(
          key: ValueKey('home'),
          child: Home(),
        ),
        if (_selected != null && _selected! > 0)
          DialogPage(
            key: const ValueKey('dialog'),  // <- Replace with `ValueKey('dialog-$selected')`, and the UI rebuilds but the state of the dialog is lost. It will print `'iniState1'`, `'iniState2'`... in `_DialogWidgetState`'s `initState`.
            child: DialogWidget(
              parameter: _selected!,
            ),
          )
        else if (_selected != null && _selected! < 0)
          MaterialPage(
            key: const ValueKey('material'),
            child: DialogWidget(
              parameter: _selected!,
            ),
          )
      ],
    );
  }

  final _navigatorKey = GlobalKey<NavigatorState>();
  @override
  GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;

  @override
  Future<void> setNewRoutePath(AppRoute configuration) async {
    _selected = configuration.location;
  }
}

class AppRouteInformationParser extends RouteInformationParser<AppRoute> {
  // This converts route state to route information.
  @override
  RouteInformation restoreRouteInformation(configuration) {
    final String location;
    if (configuration.location == null) {
      location = '/';
    } else {
      location = '/dialog/${configuration.location}';
    }
    return RouteInformation(location: location);
  }

// This converts route info to router state
  @override
  Future<AppRoute> parseRouteInformation(
      RouteInformation routeInformation) async {
    final int? selected;
    if (routeInformation.location?.startsWith('/dialog/') ?? false) {
      selected = int.parse(routeInformation.location!.split('/').last);
    } else {
      selected = null;
    }
    return AppRoute(selected);
  }
}

final delegate = AppRouter();
final informationParser = AppRouteInformationParser();

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: delegate,
      routeInformationParser: informationParser,
    );
  }
}

class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ElevatedButton(
              onPressed: () {
                delegate.selected = 1;
              },
              child: const Text('Dialog'),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                delegate.selected = -1;
              },
              child: const Text('Material'),
            ),
          ],
        ),
      ),
    );
  }
}

class DialogWidget extends StatefulWidget {
  const DialogWidget({
    required this.parameter,
    super.key,
  });

  final int parameter;

  @override
  State<DialogWidget> createState() => _DialogWidgetState();
}

class _DialogWidgetState extends State<DialogWidget> {
  @override
  void initState() {
    super.initState();
    print('initState ${widget.parameter}');
  }

  @override
  Widget build(BuildContext context) {
    return SimpleDialog(
      title: const Text('Dialog'),
      children: [
        Row(
          children: [
            IconButton(
              onPressed: () {
                delegate.selected--;
              },
              icon: const Icon(Icons.arrow_back_ios),
            ),
            Text(widget.parameter.toString()),
            IconButton(
              onPressed: () {
                delegate.selected++;
              },
              icon: const Icon(Icons.arrow_forward_ios),
            ),
          ],
        ),
      ],
    );
  }
}

class DialogPage extends Page {
  const DialogPage({
    required this.child,
    super.key,
  });

  final Widget child;

  @override
  Route createRoute(BuildContext context) {
    return _DialogRoute(
      settings: this,
      context: context,
      builder: (context) {
        ModalRoute.of(context);
        return child;
      },
    );
  }
}

class _DialogRoute extends DialogRoute {
  _DialogRoute({
    super.settings,
    required super.context,
    required super.builder,
  });

  @override
  bool canTransitionFrom(TransitionRoute previousRoute) {
    return previousRoute is! _DialogRoute;
  }

  @override
  bool canTransitionTo(TransitionRoute nextRoute) {
    return nextRoute is! _DialogRoute;
  }
}
Logs

For the logs, see the table in the "Videos" section.

Analyzing flutter_app_stable...                                         

   info • Avoid `print` calls in production code • lib/main.dart:158:5 • avoid_print

1 issue found. (ran in 5.0s)
[✓] Flutter (Channel stable, 3.3.8, on macOS 11.6.8 20G730 darwin-x64, locale en-GB)
    • Flutter version 3.3.8 on channel stable at /Users/valentin/flutter/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 52b3dc25f6 (9 days ago), 2022-11-09 12:09:26 +0800
    • Engine revision 857bd6b74c
    • Dart version 2.18.4
    • DevTools version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /usr/local/Caskroom/android-sdk/4333796
    • Platform android-33, build-tools 30.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.11+0-b60-7590822)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 13C100
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.1)
    • 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 11.0.11+0-b60-7590822)

[✓] VS Code (version 1.73.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.52.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-x64     • macOS 11.6.8 20G730 darwin-x64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 107.0.5304.110

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
Videos
With `const ValueKey('dialog')` as a key With `ValueKey('dialog-$_selected')` as a key
Video
Screen.Recording.2022-11-18.at.5.52.20.PM.mov
Screen.Recording.2022-11-18.at.6.02.10.PM.mov
Logs
flutter run -d macos
Launching lib/main.dart on macOS in debug mode...
Building macOS application...                                           
Syncing files to device macOS...                                    71ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on macOS is available at: http://127.0.0.1:62454/Sc8VIzL_xkw=/
The Flutter DevTools debugger and profiler on macOS is available at:
http://127.0.0.1:9102?uri=http://127.0.0.1:62454/Sc8VIzL_xkw=/
flutter: initState -1
flutter: initState -1
flutter: initState 1
Launching lib/main.dart on macOS in debug mode...
Building macOS application...                                           
Syncing files to device macOS...                                    71ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on macOS is available at: http://127.0.0.1:63181/P2tBIa5DOV8=/
The Flutter DevTools debugger and profiler on macOS is available at:
http://127.0.0.1:9102?uri=http://127.0.0.1:63181/P2tBIa5DOV8=/
flutter: initState -1
flutter: initState 1
flutter: initState 2
flutter: initState 3
flutter: initState 2
flutter: initState 1

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listf: routesNavigator, Router, and related APIs.found in release: 3.3Found to occur in 3.3found in release: 3.6Found to occur in 3.6frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions