-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Is there an existing issue for this?
- I have searched the existing issues
- I have read the guide to filing a bug
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:
extra: "Schubidu",extra: const MyModel("test")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) {
- 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
- 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
- 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) {
- 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
- 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
- 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) {
- 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) {
- 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
- 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
- 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) {
- 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
- 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
- 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) {
- 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!