Skip to content

StatefulShellBranch rebuilds and Go route extra null.  #129347

@SimonWeidemann

Description

@SimonWeidemann

Is there an existing issue for this?

Steps to reproduce

There are two procedures . The first has two basic setups. One with the pageBuilder method and one with the builder method. Then there are several runs with different extras you can use to add to the route. The second procedure has just on setup.

First procedure:
1 . Start the application
2. Go to tab B
3. Click View Details
4. Click on Increment (so we know that we do not have the init state)
5. Go to tab A
6. Go to tab B

The configurations for the go router are:

                   builder: (BuildContext context, GoRouterState state) {
                      print("build B with extra: ${state.extra}");
                      return DetailsScreen(
                        label: 'B',
                        extra: state.extra,
                      );
                    },
                    pageBuilder: (BuildContext context, GoRouterState state) {
                      print("build B with extra: ${state.extra}");
                      return NoTransitionPage(
                        child: DetailsScreen(
                          label: 'B',
                          extra: state.extra,
                        ),
                      );
                    },

Run both configurations with three different extras:

  1. extra: "Schubidu",
  2. extra: const MyModel("test")
  3. extra: const MyModel("test").toString()

Note that either the pageBuilder property or the builder property is set.

The second procedure is:
1 . Start the application
2. Go to tab B
3. Click View Details
4. Click on Increment (so we know that we do not have the init state)
5. Click Go Deeper
6. Click back button

Go Router config:

                   builder: (BuildContext context, GoRouterState state) {
                      print("build B with extra: ${state.extra}");
                      return DetailsScreen(
                        label: 'B',
                        extra: state.extra,
                      );
                    },

Extra setup:
extra: "Schubidu",

Expected results

Procedure one:
GoRoute with using builder: (BuildContext context, GoRouterState state) {

  1. Output with extra: "Schubidu",
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: Schubidu
[GoRouter] going to /a
[GoRouter] going to /b/details
  1. Output with extra: const MyModel("test"),
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}
[GoRouter] going to /a
[GoRouter] going to /b/details
  1. Output with extra: const MyModel("test").toJsonString(),
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}
[GoRouter] going to /a
[GoRouter] going to /b/details

GoRoute with using pageBuilder: (BuildContext context, GoRouterState state) {

  1. Output with extra: "Schubidu",
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: Schubidu
[GoRouter] going to /a
[GoRouter] going to /b/details
  1. Output with extra: const MyModel("test"),
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}
[GoRouter] going to /a
[GoRouter] going to /b/details
  1. Output with extra: const MyModel("test").toString(),
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}
[GoRouter] going to /a
[GoRouter] going to /b/details

Procedure Two:

GoRoute with using builder: (BuildContext context, GoRouterState state) {

  1. Output with extra: "Schubidu",
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: Schubidu
[GoRouter] going to /b/details/deeper

Actual results

GoRoute with using builder: (BuildContext context, GoRouterState state) {

  1. Output with extra: "Schubidu",
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: Schubidu
[GoRouter] going to /a
[GoRouter] going to /b/details
  1. Output with extra: const MyModel("test"),
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}
[GoRouter] going to /a
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: null
  1. Output with extra: const MyModel("test").toJsonString(),
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}
[GoRouter] going to /a
[GoRouter] going to /b/details

GoRoute with using pageBuilder: (BuildContext context, GoRouterState state) {

  1. Output with extra: "Schubidu",
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: Schubidu
[GoRouter] going to /a
I/flutter (23154): build B with extra: Schubidu
[GoRouter] going to /b/details
  1. Output with extra: const MyModel("test"),
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}
[GoRouter] going to /a
I/flutter (23154): build B with extra: null
[GoRouter] going to /b/details
  1. Output with extra: const MyModel("test").toString(),
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}
[GoRouter] going to /a
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: {"title":"test"}

Procedure Two:

GoRoute with using builder: (BuildContext context, GoRouterState state) {

  1. Output with extra: "Schubidu",
[GoRouter] going to /b
[GoRouter] going to /b/details
I/flutter (23154): build B with extra: Schubidu
[GoRouter] going to /b/details/deeper
I/flutter (23154): build B with extra: null
I/flutter (23154): build B with extra: null

So what we can see is that when we use a pageBuilder, we always call the build method when we return to tab b. In my opinion, we should not trigger the build again because it has already been built with the animation we want.

The next thing is that when we use a model in the extra and return to tab b. A build is triggered and the extra is null. In my opinion, the model should be there. At the moment it seems to me that the object only accepts primitives.

In the second procedure, a build is triggered when we go deeper in the tree and when we come back. In both builds the extra is then null. For me, no rebuilds should be triggered and the extra should not be null. It seems that the rebuilds could be related to this: 123570

Code sample

Code sample
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

final GlobalKey<NavigatorState> _rootNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'root');

void main() {
  runApp(NestedTabNavigationExampleApp());
}

class NestedTabNavigationExampleApp extends StatelessWidget {
  NestedTabNavigationExampleApp({super.key});

  final GoRouter _router = GoRouter(
    debugLogDiagnostics: true,
    navigatorKey: _rootNavigatorKey,
    initialLocation: '/a',
    routes: <RouteBase>[
      StatefulShellRoute.indexedStack(
        builder: (BuildContext context, GoRouterState state,
            StatefulNavigationShell navigationShell) {
          return ScaffoldWithNavBar(navigationShell: navigationShell);
        },
        branches: <StatefulShellBranch>[
          StatefulShellBranch(
            routes: <RouteBase>[
              GoRoute(
                path: '/a',
                builder: (BuildContext context, GoRouterState state) =>
                    Container(),
              ),
            ],
          ),
          StatefulShellBranch(
            routes: <RouteBase>[
              GoRoute(
                path: '/b',
                builder: (BuildContext context, GoRouterState state) =>
                    const RootScreen(
                  label: 'B',
                  detailsPath: '/b/details',
                ),
                routes: <RouteBase>[
                  GoRoute(
                      path: 'details',
                      builder: (BuildContext context, GoRouterState state) {
                        print("build B with extra: ${state.extra}");
                        return DetailsScreen(
                          label: 'B',
                          extra: state.extra,
                        );
                      },
                      /*
                      pageBuilder: (BuildContext context, GoRouterState state) {
                        print("build B with extra: ${state.extra}");
                        return NoTransitionPage(
                          child: DetailsScreen(
                            label: 'B',
                            extra: state.extra,
                          ),
                        );
                      },*/
                      routes: [
                        GoRoute(
                            path: 'deeper',
                            builder: (context, state) => Scaffold(
                                  appBar: AppBar(),
                                ))
                      ]),
                ],
              ),
            ],
          ),
        ],
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routerConfig: _router,
    );
  }
}

class ScaffoldWithNavBar extends StatelessWidget {
  const ScaffoldWithNavBar({
    required this.navigationShell,
    Key? key,
  }) : super(key: key);

  final StatefulNavigationShell navigationShell;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: NavigationBar(
        destinations: const [
          NavigationDestination(icon: Icon(Icons.home), label: 'Section A'),
          NavigationDestination(icon: Icon(Icons.work), label: 'Section B'),
        ],
        selectedIndex: navigationShell.currentIndex,
        onDestinationSelected: (int index) => _onTap(context, index),
      ),
    );
  }

  void _onTap(BuildContext context, int index) {
    navigationShell.goBranch(
      index,
      initialLocation: index == navigationShell.currentIndex,
    );
  }
}

class RootScreen extends StatelessWidget {
  const RootScreen({
    required this.label,
    required this.detailsPath,
    super.key,
  });

  final String label;

  final String detailsPath;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Root of section $label'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text('Screen $label',
                style: Theme.of(context).textTheme.titleLarge),
            const Padding(padding: EdgeInsets.all(4)),
            TextButton(
              onPressed: () {
                GoRouter.of(context).go(
                  detailsPath,
                  extra: "Schubidu",
                  // extra: const MyModel("test"),
                  //extra: const MyModel("test").toString(),
                );
              },
              child: const Text('View details'),
            ),
            const Padding(padding: EdgeInsets.all(4)),
          ],
        ),
      ),
    );
  }
}

class DetailsScreen extends StatefulWidget {
  const DetailsScreen({
    required this.label,
    this.extra,
    super.key,
  });

  final String label;

  final Object? extra;

  @override
  State<StatefulWidget> createState() => DetailsScreenState();
}

class DetailsScreenState extends State<DetailsScreen> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details Screen - ${widget.label}'),
      ),
      body: _build(context),
    );
  }

  Widget _build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text('Details for ${widget.label} - Counter: $_counter',
              style: Theme.of(context).textTheme.titleLarge),
          const Padding(padding: EdgeInsets.all(4)),
          TextButton(
            onPressed: () {
              setState(() {
                _counter++;
              });
            },
            child: const Text('Increment counter'),
          ),
          const Padding(padding: EdgeInsets.all(8)),
          TextButton(
            onPressed: () {
              GoRouter.of(context).go(
                '/b/details/deeper',
              );
            },
            child: const Text('Go deeper'),
          ),
          const Padding(padding: EdgeInsets.all(8)),
          Text(
            'Extra: ${widget.extra}',
            style: Theme.of(context).textTheme.titleMedium,
          ),
        ],
      ),
    );
  }
}

class MyModel {
  const MyModel(
    this.title,
  );

  final String title;

  @override
  String toString() {
    return jsonEncode({
      'title': title,
    });
  }

  factory MyModel.fromMap(Map<String, dynamic> map) {
    return MyModel(
      map['title'] as String,
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is MyModel &&
          runtimeType == other.runtimeType &&
          title == other.title;

  @override
  int get hashCode => title.hashCode;
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.10.5, on macOS 13.4.1 22F82 darwin-arm64, locale en-DE)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 14.3.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.2)
[✓] IntelliJ IDEA Ultimate Edition (version 2023.1.2)
[✓] Connected device (3 available)
[✓] Network resources

• No issues found!

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listfound in release: 3.10Found to occur in 3.10found in release: 3.12Found to occur in 3.12has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.r: fixedIssue is closed as already fixed in a newer version

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions