Replies: 8 comments 7 replies
-
Remove tabBar's onTap and add a listener to the controller itself. Preserve the state of the views by reusing the keys in the routes. Ideally GoRoute should have the option to override the current location instead of forcing us to go through the primitive, potentially buggy and boilerplate-y, route of navigating and preserving states. @csells can we introduce a 'overrideCurrentLocation(string newRoute)'? |
Beta Was this translation helpful? Give feedback.
-
I don't think adding à listener to the controller is solving anything, I might be missing something, but I think it introduces its own set of unwanted behaviors when calling the "go" method. But I like the idea of overriding the current location a lot. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
HI @csells! go_router_swipe.mp4@Miiite is that the issue you have trouble with? |
Beta Was this translation helpful? Give feedback.
-
No the two issues are not related. In the sample, if I replace the void _handleBookTapped(Book book) {
context.push('/book/${book.id}');
} then, on the Books page do this:
The books sample in its current form uses a context.go call to go to the |
Beta Was this translation helpful? Give feedback.
-
There are a couple of |
Beta Was this translation helpful? Give feedback.
-
Hey @Miiite. I see the changes you made. I think they're a red herring; I don't think the additional call to import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'shared/data.dart';
void main() => runApp(App());
class App extends StatelessWidget {
App({Key? key}) : super(key: key);
static const title = 'GoRouter Example: Nested Navigation';
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
);
late final _router = GoRouter(
routerNeglect: true,
routes: [
GoRoute(
path: '/',
redirect: (_) => '/family/${Families.data[0].id}',
),
// DONE
GoRoute(
path: '/person/:pid',
builder: (context, state) {
final family = Families.family(state.params['fid']!);
final person = family.person(state.params['pid']!);
return PersonScreen(family: family, person: person);
},
),
GoRoute(
path: '/family/:fid',
builder: (context, state) => FamilyTabsScreen(
key: state.pageKey,
selectedFamily: Families.family(state.params['fid']!),
),
routes: [
GoRoute(
path: 'person/:pid',
builder: (context, state) {
final family = Families.family(state.params['fid']!);
final person = family.person(state.params['pid']!);
return PersonScreen(family: family, person: person);
},
),
],
),
],
// show the current router location as the user navigates page to page; note
// that this is not required for nested navigation but it is useful to show
// the location as it changes
navigatorBuilder: (context, child) => Material(
child: Column(
children: [
Expanded(child: child!),
Padding(
padding: const EdgeInsets.all(8),
child: Text(_router.location),
),
],
),
),
);
}
class FamilyTabsScreen extends StatefulWidget {
FamilyTabsScreen({required Family selectedFamily, Key? key})
: index = Families.data.indexWhere((f) => f.id == selectedFamily.id),
super(key: key) {
assert(index != -1);
}
final int index;
@override
_FamilyTabsScreenState createState() => _FamilyTabsScreenState();
}
class _FamilyTabsScreenState extends State<FamilyTabsScreen>
with TickerProviderStateMixin {
late final TabController _controller;
@override
void initState() {
super.initState();
_controller = TabController(
length: Families.data.length,
vsync: this,
initialIndex: widget.index,
);
_controller.addListener(_tabChanged); // DONE (csells)
}
@override
void dispose() {
_controller.removeListener(_tabChanged); // DONE (csells)
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(FamilyTabsScreen oldWidget) {
super.didUpdateWidget(oldWidget);
_controller.index = widget.index;
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text(App.title),
bottom: TabBar(
controller: _controller,
tabs: [for (final f in Families.data) Tab(text: f.name)],
// onTap: (index) => _tap(context, index), // DONE (csells)
),
),
body: TabBarView(
controller: _controller,
children: [for (final f in Families.data) FamilyView(family: f)],
),
);
// DONE (csells)
void _tabChanged() {
context.go('/family/${Families.data[_controller.index].id}');
}
// DONE (csells)
// void _tap(BuildContext context, int index) =>
// context.go('/family/${Families.data[index].id}');
}
class FamilyView extends StatefulWidget {
const FamilyView({required this.family, Key? key}) : super(key: key);
final Family family;
@override
State<FamilyView> createState() => _FamilyViewState();
}
/// Use the [AutomaticKeepAliveClientMixin] to keep the state, like scroll
/// position and text fields when switching tabs, as well as when popping back
/// from sub screens. To use the mixin override [wantKeepAlive] and call
/// `super.build(context)` in build.
///
/// In this example if you make a web build and make the browser window so low
/// that you have to scroll to see the last person on each family tab, you will
/// see that state is kept when you switch tabs and when you open a person
/// screen and pop back to the family.
class _FamilyViewState extends State<FamilyView>
with AutomaticKeepAliveClientMixin {
// Override `wantKeepAlive` when using `AutomaticKeepAliveClientMixin`.
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
// Call `super.build` when using `AutomaticKeepAliveClientMixin`.
super.build(context);
return ListView(
children: [
for (final p in widget.family.people)
ListTile(
title: Text(p.name),
onTap: () => context.push('/person/${p.id}'), // DONE (Miiite)
),
],
);
}
}
class PersonScreen extends StatelessWidget {
const PersonScreen({required this.family, required this.person, Key? key})
: super(key: key);
final Family family;
final Person person;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(person.name)),
body: Text('${person.name} ${family.name} is ${person.age} years old'),
);
} |
Beta Was this translation helpful? Give feedback.
-
Hi @csells and @nullrocket ! Based on our latest exchange regarding the handling of the swipe gesture in the When you click that button, you actually never go to the Person page. What happens is that, when we trigger the The issue I originally created was related to this bug, that I currently have in my project, but I struggled to reproduce it in your examples until now. Here is the full sample code: import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'shared/data.dart';
void main() => runApp(App());
class App extends StatelessWidget {
App({Key? key}) : super(key: key);
static const title = 'GoRouter Example: Nested Navigation';
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
);
late final _router = GoRouter(
routerNeglect: true,
routes: [
GoRoute(
path: '/',
redirect: (_) => '/family/${Families.data[0].id}',
),
// DONE
GoRoute(
path: '/person/:pid',
builder: (context, state) {
final family = Families.family(state.params['fid']!);
final person = family.person(state.params['pid']!);
return PersonScreen(family: family, person: person);
},
),
GoRoute(
path: '/family/:fid',
builder: (context, state) => FamilyTabsScreen(
key: state.pageKey,
selectedFamily: Families.family(state.params['fid']!),
),
routes: [
GoRoute(
path: 'person/:pid',
builder: (context, state) {
final family = Families.family(state.params['fid']!);
final person = family.person(state.params['pid']!);
return PersonScreen(family: family, person: person);
},
),
],
),
],
// show the current router location as the user navigates page to page; note
// that this is not required for nested navigation but it is useful to show
// the location as it changes
navigatorBuilder: (context, child) => Material(
child: Column(
children: [
Expanded(child: child!),
Padding(
padding: const EdgeInsets.all(8),
child: Text(_router.location),
),
],
),
),
);
}
class FamilyTabsScreen extends StatefulWidget {
FamilyTabsScreen({required Family selectedFamily, Key? key})
: index = Families.data.indexWhere((f) => f.id == selectedFamily.id),
super(key: key) {
assert(index != -1);
}
final int index;
@override
_FamilyTabsScreenState createState() => _FamilyTabsScreenState();
}
class _FamilyTabsScreenState extends State<FamilyTabsScreen>
with TickerProviderStateMixin {
late final TabController _controller;
@override
void initState() {
super.initState();
_controller = TabController(
length: Families.data.length,
vsync: this,
initialIndex: widget.index,
);
_controller.addListener(_tabChanged); // DONE (csells)
}
@override
void dispose() {
_controller.removeListener(_tabChanged); // DONE (csells)
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(FamilyTabsScreen oldWidget) {
super.didUpdateWidget(oldWidget);
scheduleMicrotask(() => _controller.index = widget.index);
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text(App.title),
bottom: TabBar(
controller: _controller,
tabs: [for (final f in Families.data) Tab(text: f.name)],
// onTap: (index) => _tap(context, index), // DONE (csells)
),
),
body: TabBarView(
controller: _controller,
children: [for (final f in Families.data) FamilyView(family: f)],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => context.go('/family/f3/person/p1'), // DONE (Miiite)
),
);
// DONE (csells)
void _tabChanged() {
context.go('/family/${Families.data[_controller.index].id}');
}
}
class FamilyView extends StatefulWidget {
const FamilyView({required this.family, Key? key}) : super(key: key);
final Family family;
@override
State<FamilyView> createState() => _FamilyViewState();
}
/// Use the [AutomaticKeepAliveClientMixin] to keep the state, like scroll
/// position and text fields when switching tabs, as well as when popping back
/// from sub screens. To use the mixin override [wantKeepAlive] and call
/// `super.build(context)` in build.
///
/// In this example if you make a web build and make the browser window so low
/// that you have to scroll to see the last person on each family tab, you will
/// see that state is kept when you switch tabs and when you open a person
/// screen and pop back to the family.
class _FamilyViewState extends State<FamilyView>
with AutomaticKeepAliveClientMixin {
// Override `wantKeepAlive` when using `AutomaticKeepAliveClientMixin`.
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
// Call `super.build` when using `AutomaticKeepAliveClientMixin`.
super.build(context);
return ListView(
children: [
for (final p in widget.family.people)
ListTile(
title: Text(p.name),
onTap: () => context.push('/person/${p.id}'),
),
],
);
}
}
class PersonScreen extends StatelessWidget {
const PersonScreen({required this.family, required this.person, Key? key})
: super(key: key);
final Family family;
final Person person;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(person.name)),
body: Column(children: [
Text('${person.name} ${family.name} is ${person.age} years old'),
ElevatedButton(
onPressed: () => context.go('/family/f2/person/p1'),
child: Text('Go to first person of F2'),
)
]),
);
} |
Beta Was this translation helpful? Give feedback.
-
Hello,
I am facing a nested navigation scenario that requires me to "push" child pages, instead of using
GoRouter.go
.The children widgets (equivalent to the
Person
widget in the example project) are used in multiple pages, and are unaware of the current context. So it requires me to push the widget onto the navigation stack, no matter where the user is right now in the app.So if I update the nested_navigation.dart sample file with this kind of changes, it looks like this:
The only things that changed, is that now I
push
the Person page on click, and therefore I declared a/person/xx
route in the Router declaration.The issue with that scenario is that, when the user swipes left and right in the TabBar, the index of the current tabbar changes, but GoRouter is unaware of those changes, so the URL stays the same.
/family/f1
.Addams
family, but my URL is stillfamily/f1
.Wednesday
. So I nowpush
the/person/p4
onto the navigation stack/family/f1/person/p4
which does not exist.It looks like we are missing a way of "synchronising" the GoRouter URL with the currently displayed screen.
In my scenario, I would need a way to tell GoRouter that the URL is now
family/f2
when the user swipes right.My first reaction was to add a
GoRouter.go
call, when the user swipes, by reacting to the Controller events.But If I do that, when I will trigger "deeplink" navigation by calling for example
context.go('/family/f3/person/p2')
, then after this URL will be interpreted and the screen loaded, the family's page Controller will trigger a value change, so I will trigger aGoRouter.go
call to try to synchronise my screen's state with GoRouter's state, and that will make me go back to/family/f3
.It's not necessarily an easy problem to describe, so I hope I am making sense here.
Thanks for the help !
Beta Was this translation helpful? Give feedback.
All reactions