-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Preserve stack when switching between nested routes #32
Comments
When you say "maintain its route stack when switching back." you mean that when you tap the bottom navigation bar, the pushed path should be different depending on where you left the tab you are going to is that right? Also could you tell me if you are targeting the web or not at all? |
Yes
No. I know that this pattern (i.e. multiple parallel stacks) is very common on mobile but doesn't really fit with browser navigation. I should also mention that my tabs need to be able to share some routes (i.e. you can reach the same screen from multiple tabs). |
Do you mean that "AllKriyasScreen" and "KriyaDetailsScreen" are the same for the three tabs? Let's ignore "KriyaDetailsScreen"and talk about "AllKriyasScreen" since both are the same navigation-wise. Sorry for all the question but I would rather understand what you want than showing you 10 different methods which don't meet your needs |
Yes. These are just two screens, but my actual app has >50, any of which could be pushed onto the stack of any tab. The primary difference between the tabs is the starting screen. For example, my "practice" tab will start on the "PracticeScreen", "listen" tab will start on the "ListenScreen" screen, and "learn" will start on the "LearnScreen". So, let's assume we have an "AlbumDetailsScreen". This is most likely to be pushed onto the "listen" tab stack since the starting "ListenScreen" has more navigation paths that lead to "AlbumDetailsScreen". However, the user might be practicing an exercise on the "practice" tab and tap into a linked album, in which case it would push the "AlbumDetailsScreen" onto the "practice" tab's stack.
No. Each tab should have an independent stack. However, the same route could be pushed onto any of the tabs. For example, my app might have the following route stack state:
Assuming I'm on the
No worries. I appreciate the assistance. |
Ok so I think I found a solution. I don't have access to my computer until Monday so I'll try to write down my idea but code snippets will be briefs. The idea is to have a map inside the class containing your Here is the initialisation of the map: var tabStack = {
'practice': '/practice',
'listen' : '/listen',
'learn' : '/learn',
} To keep the stack in sync,yyou can you three VNester(
path: null,
widgetBuilder: (child) => AppTabsScaffold(child: child, tabStack: tabStack),
nestedRoutes: [
VGuard(
afterEnter: (_, __, to) => tabStack['profile'] = to;
afterUpdate: (_, __, to) => tabStack['profile'] = to;
VWidget(
widget: PracticeScreen(),
path: '/practice',
stackedRoutes: _tabRoutes,
),
),
// same for listen
// same for learn
],
) Also note that I give tabStack to And then when you press tap on the bottom navigation bar: onTap: (index) {
switch (index) {
case 0:
return context.vRouter.push(tabStack['profile'])
// same for 1 and 2
}
}, Also note that your routes in Anyway hope this helps! |
Hey Lulu, would this method be able to preserve state between each stack as well? It seems like it's only remembering the location of each stack, not the state of each stack |
Yes this would only remember the location but I think this is what @smkhalsa wants. Remembering the state is not the job of VRouter I think, this can be achieve in (at least) two ways though:
What is comes down to is what the needs are. Looking at this very simple example I would use indexed stack but maybe the is not enough |
OK, good to know - yes I agree, I think indexed stack would be the best way to accomplish that |
I think this is true in general (the state should be hoisted above the view), but is there really no way we could just signal that we want to retain state for a route? This would be quite nice for things like TextFields and ScrollingLists, but also allows you to use StatefulWidget to store state if you want. Consider a simple example like this, it would be quite nice to maintainState somehow in the built widgets:
As you switch between the routes, it is always re-running initState, having these routes be able to preserve state would be very useful. |
The question of state handling is interesting and gave me a lot of thoughts. Let my first give you two links showing what the React team is thinking about this:
This does not mean that they are right, but they sure partially guided me away from doing it. Also when I think about state restoration I think about 2 different scenarios:
1. A widget has some state when it is left, this state should be restored if it appears ever againThis is the case of your example. As you perfectly illustrated, this is often use with application fetch data. In this case as you said this is more the job of a state management library. In some cases, those situations are said to be common enough that flutter has built in state restoration mechanism, such as 2. Some information relative to the current route should be stored and restored if the user were to come back to that route using the browser back buttonI think this is something really interesting in terms of usx which often gets overlooked, and is really hard to solve without the help of This situation arises in github for example. See those situations:
This is exactly what |
The various restoration are definitely cool, and necessary, but also by their nature very cumbersome and lots of work to implement right and test. I was thinking of something simpler, where routes are simply maintained in memory and not rebuilt. If VRouter is somehow able to mark routes in memory, and rather than rebuilding the route, pass the previous instance, there would be no restoration needed and we could bring a really slick app paradigm over to the web. React can't really do this as they are still bound tightly to the single-web page paradigm, but in Flutter we have a runtime, and we can stash things in ram or hide entire routes off-screen if we wanted. As a point of comparison here, you could compare twitter.com and the Twitter App on Android.
I don't quite understand the suggestions to use Indexed stack for complicated routing needs, are you suggesting all the routes collapse down to one? So instead of having a /home, /explore/, /settings, we have simply |
I understand what you want and why you think this might be included in If you have any idea, maybe show me an example without In any case, even if this could work, I am still not sure this should be part of import 'package:flutter/material.dart';
import 'package:vrouter/vrouter.dart';
main() {
runApp(SimpleRouterTest());
}
class SimpleRouterTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return VRouter(
initialUrl: "/home",
routes: [
VNester(
path: null,
widgetBuilder: (child) => MyScaffold(child: child),
nestedRoutes: [
VWidget(
path: '/home',
widget: HomeScreen(),
),
VWidget(
path: '/settings',
widget: SettingsScreen(),
),
],
),
],
);
}
}
class MyScaffold extends StatelessWidget {
final Widget child;
const MyScaffold({Key? key, required this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
final currentIndex = (context.vRouter.url == '/home') ? 0 : 1;
return Scaffold(
body: IndexedStack(
children: [
HomeScreen(),
SettingsScreen(),
],
index: currentIndex,
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index) => context.vRouter.push((index == 0) ? '/home' : '/settings'),
currentIndex: currentIndex,
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
],
),
);
}
}
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Type and navigate to see the state of the TextField be preserved'),
TextField(),
],
);
}
}
class HomeScreen extends StatelessWidget {
final colors = [
Colors.transparent,
Colors.redAccent,
Colors.greenAccent,
Colors.lightBlueAccent,
Colors.amberAccent,
];
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: ListView.builder(
itemBuilder: (_, index) =>
Container(
color: colors[index % colors.length],
height: 50,
)
),
),
Align(
alignment: Alignment.topCenter,
child: Text('Scroll and navigate to see the state of the ListView be preserved'),
),
],
);
}
} |
I believe the |
And how can you make it |
Pages are not Widgets. You could probably just create the page outside of a Widget build method (e.g. in the initState of a StatefulWidget). |
In any case, even if |
I think it has to be the responsibility of the routers, who else could cache these routes for us but the Router? Otherwise we just end up re-writing our own router-like sub-tree, relagating the VRouter to just passing around paths and query strings. That doesn't mean it's easy though, I know Flutter probably makes this way harder than it needs to be. This inability to save state unless something is on the Tree is such a frustrating limitation of the framework :/ I'll try and write some demo code today, but high level I think it needs to work something like:
Unless I'm missing something, in this example, the children passed into your routes are never used? VRouter is basically just taking a round about way of passing us the current path args so we can essentially route the path internally. This would be quite confusing in practice as you're declaring HomeView() twice, one of them is never used, almost easier to just use onGenerateRoute with a single Page at this point. |
I'm looking forward to that!
True, this is useful for simple issues, for example if you only need the state to be persistent in a scaffold. I agree that there are many more use cases that are way to complex to use that. If it's too complex it means that you have to use a state management library I think. |
Having an offstage or indexed stack to render multiple navigators in parallel has been a widely-used solution for persisting state, all the way back to when flutter was just starting to grow. A lot of articles and discussion on this since it's a pretty common use-case for mobile apps: https://medium.com/flutter/getting-to-the-bottom-of-navigation-in-flutter-b3e440b9386 I feel like IndexedStack works great - why can't it scale for more complex cases? The router is the one preserving the state. |
But again, it's not about the shared state, it's everything else. Hoisting the state above my views, and having each view grab the latest data is pretty trivial. But things like remembering scroll position is a headache, and other small things like the contents of TextInputs or maybe an intro animation we don't want running again and again. I do see what you mean though, that solving these low level problems might be the lesser of 2 evils. I wonder if the new restoration API can be of any help here. |
Ok, so here's a proof of concept "Persistent Router", I have not looked at your internal code, this is just to illustrate that the core concept is not really far fetched.
Full code below, but the core of it is: Widget build(BuildContext context) {
/// Try and find a known route for the current path
RouteConfig matchingRoute = List<RouteConfig>.from(widget.routes)
.firstWhere((element) => element.path == widget.currentPath, orElse: () => null);
if (matchingRoute == null) matchingRoute = pageNotFoundRoute;
// Remove any known routes that are not persistent
knownRoutes.removeWhere((key, value) => value.maintainState == false);
// Add the new route to our list of known routes
knownRoutes[matchingRoute.path] = matchingRoute;
// Pass all known pages to IndexedStack, but only render the current one
List<RouteConfig> allRoutes = knownRoutes.values.toList();
int currentIndex = allRoutes.indexWhere((r) => r.path == matchingRoute.path);
return IndexedStack(
index: currentIndex,
children: allRoutes.map((r) => r.page).toList(),
);
} C42HrHUPu6.mp4class PersistentRouterDemo extends StatefulWidget {
static PageInfo info = PageInfo(title: "Persistent Router");
@override
_PersistentRouterDemoState createState() => _PersistentRouterDemoState();
}
class _PersistentRouterDemoState extends State<PersistentRouterDemo> {
String url = "page1";
void setUrl(String value) => setState(() => url = value);
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
TextButton(onPressed: () => setUrl("page1"), child: Text("Page1")),
TextButton(onPressed: () => setUrl("page2"), child: Text("Page2")),
TextButton(onPressed: () => setUrl("page3"), child: Text("Page3")),
TextButton(onPressed: () => setUrl("bad link"), child: Text("bad link")),
],
),
Expanded(
child: PersistentRouter(currentPath: url, routes: [
RouteConfig(path: "page1", page: SomeStatefulPage("Page1"), maintainState: true),
RouteConfig(path: "page2", page: SomeStatefulPage("Page2"), maintainState: true),
RouteConfig(path: "page3", page: SomeStatefulPage("Page3")),
]))
],
);
}
}
class RouteConfig {
RouteConfig({@required this.path, @required this.page, this.maintainState = false});
final String path;
final Widget page;
final bool maintainState;
}
class PersistentRouter extends StatefulWidget {
final String currentPath;
final List<RouteConfig> routes;
const PersistentRouter({Key key, @required this.currentPath, @required this.routes}) : super(key: key);
@override
_PersistentRouterState createState() => _PersistentRouterState();
}
class _PersistentRouterState extends State<PersistentRouter> {
Map<String, RouteConfig> knownRoutes = {};
RouteConfig pageNotFoundRoute = RouteConfig(path: "404", page: Center(child: Text("404")));
@override
Widget build(BuildContext context) {
/// Try and find a known route for the current path
RouteConfig matchingRoute = List<RouteConfig>.from(widget.routes)
.firstWhere((element) => element.path == widget.currentPath, orElse: () => null);
if (matchingRoute == null) matchingRoute = pageNotFoundRoute;
// Remove any known routes that are not persistent
knownRoutes.removeWhere((key, value) => value.maintainState == false);
// Add the new route to our list of known routes
knownRoutes[matchingRoute.path] = matchingRoute;
// Pass all known pages to IndexedStack, but only render the current one
List<RouteConfig> allRoutes = knownRoutes.values.toList();
int currentIndex = allRoutes.indexWhere((r) => r.path == matchingRoute.path);
return IndexedStack(
index: currentIndex,
children: allRoutes.map((r) => r.page).toList(),
);
}
}
class SomeStatefulPage extends StatefulWidget {
const SomeStatefulPage(this.title, {Key key}) : super(key: key);
final String title;
@override
_SomeStatefulPageState createState() => _SomeStatefulPageState();
}
class _SomeStatefulPageState extends State<SomeStatefulPage> {
List<int> items;
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 1), () => setState(() => items = List.generate(100, (index) => index)));
}
@override
Widget build(BuildContext context) {
if (items == null) return Center(child: CircularProgressIndicator());
return Column(
children: [
Text(widget.title, style: TextStyle(fontSize: 40)),
TextField(),
Expanded(child: ListView.builder(itemBuilder: (_, index) => Text("ITEM: $index"))),
],
);
}
} |
Also I was wrong about indexed stack, it is a little smarter than just moving them offscreen, but at it's core it's extremely simple:
Probably wouldn't be too hard to implement an AnimatedIndexedStack that could show the |
Another proof of concept I've been playing with, it extends on the idea above, creating a "PathedStack", this is basically an indexed stack, that takes a path and a list of entries (builders), additionally it wraps a key around each entry, using Using that, I can make nested stacks, that pretty much work as you'd expect with regards to nested routing and maintainState: return MainScaffold( // Main scaffold has the first row of tab btns
child: PathStack(
currentPath: currentPath,
entries: [
PathStackEntry(path: "home", builder: (_) => SomeStatefulPage("HOME")),
PathStackEntry(
path: "settings/",
builder: (_) => SettingsScaffold( // Setting scaffold has a nested set of tab menus
child: PathStack(
parentPath: "settings/",
currentPath: currentPath,
entries: [
PathStackEntry(
path: "page1",
builder: (_) => SomeStatefulPage("page1 ${Random().nextInt(999)}", key: ValueKey(0)),
),
PathStackEntry(
path: "page2",
builder: (_) => SomeStatefulPage("page2 ${Random().nextInt(999)}", key: ValueKey(1)),
),
],
),
),
),
],
),
); This gives us the effective site map:
Where all pages are persistent, and maintain state, but can also take new args (as the Random().nextInt) shows. 6U0ruylBJM.mp4I realize this is heading in quite a different direction than VRouter currently, but might inspire some more thoughts. |
So I have done a lot of research on the topic lately, and it appears to me that the new state restoration framework would only work for when the app in killed in the background then resumed. The flutter team is looking into extending it to browser back/forward state restoration. Hence I am really not sure this is something to be looked into when searching for a state restoration option. The other option is, as I already demonstrated and that @esDotDev did as well to use something which keep the entire widget state into memory, by doing as
Each of these issues is bad:
Therefore this is not an option for me. I could also build something from the ground up, momentum and navigation_saver are two examples. Looking at them really make me say that this is a good idea for me. There are trying to bring something else rather than integrate themselves. Which can be good but is not my ideology, since this means a lot of learning and re-implementation for the developer. In the end, what I would love would be to be able to use the new |
Interesting points, some thoughts:
|
I've logged an issue here for StateRestoration at runtime, as this would certainly simplify things at the router level: |
Thanks, I would certainly keep an eye on this issue since as I said this would be a viable thing to implement in VRouter. Thanks for taking the time to fill up the issue and warn me. |
I'll close this since I think we can track the flutter issue. Regarding state restoration using Thanks again for the time taken to discuss this 😊 |
@esDotDev in particular, you might be interested by this ! I just found out that it has always been possible to use Here is the code:import 'package:flutter/material.dart';
import 'package:vrouter/vrouter.dart';
void main() {
runApp(
VRouter(
debugShowCheckedModeBanner: false,
routes: [
VNester(
path: '/',
widgetBuilder: (child) => MyScaffold(child), // Child is the widget from nestedRoutes
nestedRoutes: [
VWidget(path: null, widget: HomeScreen()), // null path matches parent
],
),
VNester(
path: '/',
widgetBuilder: (child) => MyScaffold(child), // Child is the widget from nestedRoutes
nestedRoutes: [
VWidget(
path: 'profile',
widget: ProfileScreen(),
stackedRoutes: [VWidget(path: 'settings', widget: SettingsScreen())],
),
],
),
],
),
);
}
abstract class BaseWidget extends StatefulWidget {
String get title;
String get buttonText;
String get to;
@override
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
bool isChecked = false;
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.title),
SizedBox(height: 50),
ElevatedButton(
onPressed: () => context.vRouter.to(widget.to),
child: Text(widget.buttonText),
),
SizedBox(height: 50),
Checkbox(
value: isChecked,
onChanged: (value) => setState(() => isChecked = value ?? false),
),
],
),
),
);
}
}
class MyScaffold extends StatefulWidget {
final Widget child;
const MyScaffold(this.child);
@override
_MyScaffoldState createState() => _MyScaffoldState();
}
class _MyScaffoldState extends State<MyScaffold> {
List<Widget> tabs = [Container(), Container()];
List<String> tabsLastVisitedUrls = ['/', '/profile'];
@override
Widget build(BuildContext context) {
final currentIndex = context.vRouter.url.contains('profile') ? 1 : 0;
// Populate the tabs when needed
tabs[currentIndex] = widget.child;
// Populate tabs last visited url
tabsLastVisitedUrls[currentIndex] = context.vRouter.url;
return Scaffold(
body: IndexedStack(
index: currentIndex,
children: tabs,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (value) => context.vRouter.to(tabsLastVisitedUrls[value]),
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Profile'),
],
),
);
}
}
class HomeScreen extends BaseWidget {
@override
String get title => 'Home';
@override
String get buttonText => 'Go to Profile';
@override
String get to => '/profile';
}
class ProfileScreen extends BaseWidget {
@override
String get title => 'Profile';
@override
String get buttonText => 'Go to Settings';
@override
String get to => '/profile/settings';
}
class SettingsScreen extends BaseWidget {
@override
String get title => 'Settings';
@override
String get buttonText => 'Pop';
@override
String get to => '/profile';
} The "trickiest" part is the duplication of Apart from that it's just basic logic:
I don't know why I did not think of it before because it's been possible since Let me know what you think ! |
Oh that's pretty neat! I think the whole index management thing is probably a headache to maintain, but maybe it could be made more dynamic?
This is essentially how my |
The index is pretty easy to maintain because you only care about the first route. Though It's even easier if you pass the See this updated example:import 'package:flutter/material.dart';
import 'package:vrouter/vrouter.dart';
void main() {
runApp(
VRouter(
debugShowCheckedModeBanner: false,
routes: [
VNester(
path: '/',
widgetBuilder: (child) => MyScaffold(child, currentIndex: 0),
nestedRoutes: [
VWidget(path: null, widget: HomeScreen()),
],
),
VNester(
path: '/',
widgetBuilder: (child) => MyScaffold(child, currentIndex: 1),
nestedRoutes: [
VWidget(
path: 'profile',
widget: ProfileScreen(),
stackedRoutes: [VWidget(path: 'settings', widget: SettingsScreen())],
),
],
),
],
),
);
}
abstract class BaseWidget extends StatefulWidget {
String get title;
String get buttonText;
String get to;
@override
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
bool isChecked = false;
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.title),
SizedBox(height: 50),
ElevatedButton(
onPressed: () => context.vRouter.to(widget.to),
child: Text(widget.buttonText),
),
SizedBox(height: 50),
Checkbox(
value: isChecked,
onChanged: (value) => setState(() => isChecked = value ?? false),
),
],
),
),
);
}
}
class MyScaffold extends StatefulWidget {
final Widget child;
final int currentIndex;
const MyScaffold(this.child, {required this.currentIndex});
@override
_MyScaffoldState createState() => _MyScaffoldState();
}
class _MyScaffoldState extends State<MyScaffold> {
List<Widget> tabs = [Container(), Container()];
List<String> tabsLastVisitedUrls = ['/', '/profile'];
@override
Widget build(BuildContext context) {
// Populate the tabs when needed
tabs[widget.currentIndex] = widget.child;
// Populate tabs last visited url
tabsLastVisitedUrls[widget.currentIndex] = context.vRouter.url;
return Scaffold(
body: IndexedStack(
index: widget.currentIndex,
children: tabs,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: widget.currentIndex,
onTap: (value) => context.vRouter.to(tabsLastVisitedUrls[value]),
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Profile'),
],
),
);
}
}
class HomeScreen extends BaseWidget {
@override
String get title => 'Home';
@override
String get buttonText => 'Go to Profile';
@override
String get to => '/profile';
}
class ProfileScreen extends BaseWidget {
@override
String get title => 'Profile';
@override
String get buttonText => 'Go to Settings';
@override
String get to => '/profile/settings';
}
class SettingsScreen extends BaseWidget {
@override
String get title => 'Settings';
@override
String get buttonText => 'Pop';
@override
String get to => '/profile';
} What's great is that this really fits VRouter since no change to the package is needed. |
Something even better is that this approach does not seem to have any restriction since it makes little assumption of what is needed. Here is an "advanced" exampleimport 'package:flutter/material.dart';
import 'package:vrouter/vrouter.dart';
void main() {
runApp(
VRouter(
debugShowCheckedModeBanner: false,
routes: [
VNester(
path: '/',
widgetBuilder: (child) => MyScaffold(child, currentIndex: 0),
nestedRoutes: [
VWidget(
path: null,
key: ValueKey('Home'),
widget: HomeScreen(),
stackedRoutes: [
VNester(
path: null,
widgetBuilder: (child) => MyTabs(child, currentIndex: 0),
nestedRoutes: [
VWidget(path: 'red', widget: ColorScreen(color: Colors.redAccent, title: 'Red'))
],
),
VNester(
path: null,
widgetBuilder: (child) => MyTabs(child, currentIndex: 1),
nestedRoutes: [
VWidget(path: 'green', widget: ColorScreen(color: Colors.greenAccent, title: 'Green'))
],
),
],
),
],
),
VNester(
path: '/',
widgetBuilder: (child) => MyScaffold(child, currentIndex: 1),
nestedRoutes: [
VWidget(
path: 'profile',
widget: ProfileScreen(),
stackedRoutes: [VWidget(path: 'settings', widget: SettingsScreen())],
),
],
),
],
),
);
}
class BaseWidget extends StatefulWidget {
final String title;
final String buttonText;
final String to;
BaseWidget({required this.title, required this.buttonText, required this.to});
@override
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
bool isChecked = false;
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.title),
SizedBox(height: 50),
ElevatedButton(
onPressed: () => context.vRouter.to(widget.to),
child: Text(widget.buttonText),
),
SizedBox(height: 50),
Checkbox(
value: isChecked,
onChanged: (value) => setState(() => isChecked = value ?? false),
),
],
),
),
);
}
}
class MyScaffold extends StatefulWidget {
final Widget child;
final int currentIndex;
const MyScaffold(this.child, {required this.currentIndex});
@override
_MyScaffoldState createState() => _MyScaffoldState();
}
class _MyScaffoldState extends State<MyScaffold> {
List<Widget> tabs = [Container(), Container()];
List<String> tabsLastVisitedUrls = ['/', '/profile'];
@override
Widget build(BuildContext context) {
// Populate the tabs when needed
tabs[widget.currentIndex] = widget.child;
// Populate tabs last visited url
tabsLastVisitedUrls[widget.currentIndex] = context.vRouter.url;
return Scaffold(
body: IndexedStack(
index: widget.currentIndex,
children: tabs,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: widget.currentIndex,
onTap: (value) => context.vRouter.to(tabsLastVisitedUrls[value]),
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Profile'),
],
),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BaseWidget(title: 'Home', buttonText: 'Go to Color Tabs', to: '/red');
}
}
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BaseWidget(title: 'Settings', buttonText: 'Pop', to: '/profile');
}
}
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BaseWidget(title: 'Profile', buttonText: 'Go to Settings', to: '/profile/settings');
}
}
class MyTabs extends StatefulWidget {
final Widget child;
final int currentIndex;
const MyTabs(this.child, {required this.currentIndex});
@override
_MyTabsState createState() => _MyTabsState();
}
class _MyTabsState extends State<MyTabs> with SingleTickerProviderStateMixin {
late final tabController = TabController(
initialIndex: widget.currentIndex,
length: tabs.length,
vsync: this,
);
// We use this as the index to easily fetch the new widget when in comes into view
int get tabControllerIndex => tabController.index + tabController.offset.sign.toInt();
List<Widget> tabs = [Container(), Container()];
@override
Widget build(BuildContext context) {
// Sync the tabController with the url
if (!tabController.indexIsChanging && tabControllerIndex != widget.currentIndex)
tabController.animateTo(widget.currentIndex);
// Populate the tabs when needed
tabs[widget.currentIndex] = widget.child;
tabs = List.from(tabs); // Needed so that TabBarView updates its children
return NotificationListener<ScrollNotification>(
onNotification: (_) {
// Syncs the url with the tabController
if (tabControllerIndex != widget.currentIndex)
context.vRouter.to(tabControllerIndex == 0 ? '/red' : '/green');
return false;
},
child: TabBarView(
controller: tabController,
children: tabs,
),
);
}
}
class ColorScreen extends StatefulWidget {
final Color color;
final String title;
const ColorScreen({required this.color, required this.title});
@override
_ColorScreenState createState() => _ColorScreenState();
}
class _ColorScreenState extends State<ColorScreen>
with AutomaticKeepAliveClientMixin<ColorScreen> {
bool isChecked = false;
@override
Widget build(BuildContext context) {
super.build(context);
return Container(
color: widget.color,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.title),
SizedBox(height: 50),
ElevatedButton(
onPressed: () => context.vRouter.to('/'),
child: Text('Pop'),
),
SizedBox(height: 50),
Checkbox(
value: isChecked,
onChanged: (value) => setState(() => isChecked = value ?? false),
),
],
),
),
);
}
@override
bool get wantKeepAlive => true;
} You can see that on top of the previous example I added:
What's pretty neat is that once more this does not require anything new from VRouter, you can ask google "How to keep the state in a TabBarView" and you are likely to be able to come up with such a design. |
My app uses tabbed navigation, in which each tab maintains its own independent stack. When switching between tabs, the route stack should be preserved for the given tab.
Here is what I have so far. However, I can't figure out how to get each tab to maintain its route stack when switching back.
Any guidance would be much appreciated.
Where
AppTabScaffold
isThe text was updated successfully, but these errors were encountered: