I am using Flutter auto_route for my nested navigation, where I would like to pass data (A dynamic string for the AppBar-title and a Widget for the floatingActionButton) from a nested route to an outer route (two levels up from the according to the route tree).
The navigation (tree) has the following structure:
#MaterialAutoRouter(
routes: <AutoRoute>[
AutoRoute(
page: MainNavigationView,
children: [
AutoRoute(
path: 'manage',
page: EmptyRouterPage,
name: 'ManageRouter',
children: [
AutoRoute(
path: 'object',
page: ObjectView,
initial: true,
),
AutoRoute(
path: 'detail',
page: ObjectDetailView,
),
]
)
]
)
]
)
My Page uses nested navigation, where MainNavigationView represents the Scaffold which holds an AppBar and SpeedDial as floatingActionButton of the Scaffold:
class MainNavigationView extends StatefulWidget {
...
}
class _MainNavigationViewState extends State<MainNavigationView> {
final GlobalKey<ScaffoldState> _scaffoldkey = GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return AutoTabsRouter(
routes: [
const ManageRouter(),
...
],
builder: (context, child, __) {
final tabsRouter = AutoTabsRouter.of(context);
return Scaffold(
key: _scaffoldkey,
appBar: AppBar(...), //Title needs to be set dynamically
body: child,
floatingActionButton: SpeedDial(
... //This needs to be set dynamically
),
...
);
},
);
}
...
}
Inside the route ManageRouter, I can navigate from ObjectView to ObjectDetailView. From both views, I need to pass a dynamic string for the AppBar and the necessary Objects for the floatingActionButton.
The only solution I came up with was a Provider, for the MainNavigationView which would allow me the send the data in a decoupled manner. But it seems like overkill, for something this general.
UPDATE:
I have looked into several other solutions. None of them had a scenario where there was a scaffold in the parent and the nested router was passing values to the parent Scaffold. What I have seen in other examples of other router packages (such as go_router) was a Scaffold in the MainNavigationView with a bottomNavigationBar property set and no appBar or floatingBottons property set. And the nested content (Object/ObjectDetail via ManageRouter) has its own Scaffold with properties set for appBar and bottomNavigationBar.
I have tried solving it with the state management solutions such as Provider but besides problems with triggering a notifyListeners()-action while building (because I placed the notifyListeners() action inside the build-method of the nested content) I had a problem regarding the stack management. Handling that logic, with the side effects of a bottomNavigationBar, which has its own stack, I figured that it is an architectural problem.
I still thank you alls for your effort and interest!
I think AutoRouter package doesn't have any functionality to pass a parameter between Nested routes. Your situation can be solved much easier!
AutoTabsRouter have field named activeIndex.
And this field will updates in AutoTabsRouter builder method when you select an page from your MainNavigationView children.
I use this in my applications to display the selected page icon in the bottomNavigationBar and selected page title in appBar.
Solution for your situation:
class MainNavigationView extends StatefulWidget {
...
}
class _MainNavigationViewState extends State<MainNavigationView> {
final GlobalKey<ScaffoldState> _scaffoldkey = GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return AutoTabsRouter(
routes: [
const ManageRouter(),
...
],
builder: (context, child, __) {
final tabsRouter = AutoTabsRouter.of(context);
return Scaffold(
key: _scaffoldkey,
appBar: AppBar(
title: tabsRouter.activeIndex == 1? 'ObjectViewTitle' : 'ObjectDetailViewTitle'
), //Title needs to be set dynamically
body: child,
floatingActionButton: SpeedDial(
child: tabsRouter.activeIndex == 1? ObjectViewFab() : ObjectDetailViewFab()
),
...
);
},
);
}
...
}
Related
I'm writing a flutter windows application using fluent_ui package.
I have NavigationView with NavigationPage and NavigationBody items.
On some NavigationBody item, I want to navigate to the other page (for example from page app/cars/ to app/cars/id) but leave the same NavigationPane state (which mean changing only a NavigationBody widget).
How can I achieve this? Only by using some kind of SetState() which totally changes the content of the widget or some solutions with using Navigator?
Basic page structure:
- NavigationView
- NavigationPane
- NavigationBody
So. I found a solution to this problem. My solution is based on using Navigator as a base widget. In NavigationView as a NavigationBody item used not concrete page (widget), but Navigator which takes care of inner navigation. So Push() method changes only the NavigationBody of NavigationView and not the entire window of the application.
Here is Code example for this. Hope someone will find this helpful:
nav_view_page.dart
NavigationView(
appBar: //some app bar
pane: // your NavigationPane
content: NavigationBody(
index: _selectedIndex,
children: [
//some other body items
CarsPageNavigator(
navigatorKey: GlobalKey<NavigatorState>(),
carsController: widget.carsController,
), //this is our navigator
],
),
);
custom_navigator.dart
class CarsPageNavigator extends StatelessWidget {
const CarsPageNavigator(
{Key? key, required this.navigatorKey, required this.carsController})
: super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final CarsController carsController;
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
initialRoute: '/car',
onGenerateRoute: (RouteSettings routeSettings) {
return FluentPageRoute(
settings: routeSettings,
builder: (context) {
return getPage(routeSettings.name);
});
});
}
Widget getPage(String? url) {
switch (url) {
case CarsManagementPage.routeName:
{
return CarsManagementPage(carsController: carsController);
}
case CarManagementPage.routeName:
return CarManagementPage(controller: carsController);
default:
return CarsManagementPage(carsController: carsController);
}
}
}
In this code in OnGenerateRoute method returns page that you want to navigate to.
To navigate to other page from some inner page use pushNamed(or other named push methods of Navigator):
Navigator.pushNamed(context, CarManagementPage.routeName, arguments: car);
small remark: some "basic" code is skipped to make answer smaller
I would like to use Flutter’s (2.0) navigation for routing on my mobile app. I cannot find cookbook examples and followed the recommended guide, implementing the nested router example.
NOTE ===
If a rendered view has a unique route (uri) within the app I call it a Page.
If it does not have a unique route, I call it a Screen.
========
The base router selects between pages in the app. The nested router in the “Resource Dashboard” uses the resourceViewState object to select the screen to render within the Resource Dashboard” Page. Just by using the selectedIndex as below, I can change the screen depending on which index the user has selected in a Material Design Drawer.
As a result, when the user is on any non-default screen (i.e. Details A, Details B) in the above diagram, and there is a pop event, the user is returned to the default screen. If the user pops from the default screen, they are returned to the “Select Resource” Page outside of the Nested Router. But I have one more sticky case to handle (or maybe I am not handling these cases well :))
#override
Widget build(BuildContext context) {
int selectedIndex = resourceViewState.selectedIndex;
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey(DEFAULT_PAGE),
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(‘DEFAULT’),
),
drawer: ResourceDrawer(
resourceViewState: resourceViewState,
navKey: navigatorKey,
),
body: DefaultPage(),
),
),
if (selectedIndex != DEFAULT_PAGE) ...[
MaterialPage(
key: ValueKey(selectedIndex),
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(_getTitle(selectedIndex)),
),
drawer: ResourceDrawer(
deviceState: deviceState,
navKey: navigatorKey,
),
body: _getScreen(selectedIndex),
),
),
],
],
onPopPage: (route, result) {
if (result == "return") {
print("return ");
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},
);
}
I want to navigate the user from an Alert Dialog to the “Select Resource” page when a button is clicked in the dialog. To do so I must (see 1, 2, 3 in diagram).
3 seems to be handled by the base router if the user pops from the default screen, but I also need to do so from an Alert Dialog.
Pop the Alert Dialog
Pop the Material Design Drawer
Pop from the Nested Router to the Base Router.
This has the effect shown by the red arrow in the diagram.
I can simply use Navigator.of(context).pop() for the first 2. I am pretty sure that the Navigator used here is different from that used by the Nested Router (would love some details here). I believe this is the case, because onPopPage is not called for the NestedRouter on these events.
For 3) I have tried this strategy:
a) Call navKey.currentState!.pop(“disconnected”) from the Navigation Drawer. I pass in the navKey of the Nested Router as shown in the code above.
b) Now the onPopPage listener registered with the nested router receives the result from this pop event.
onPopPage: (route, result) {
if (result == "return") {
print("return ");
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},
When I see result == "return" then I should navigate from the “Resource Dashboard” Page to the “Select Resource” Page. But I am not sure how to do it nor if it is even a good strategy to use different views on the same route (tangent).
This is a working solution. I pass a reference to the parent navigator key, then call pop from it. I would prefer for it to be entirely declarative but I am not sure how to implement with the nested navigator pattern.
class ResourcePage extends StatefulWidget {
ResourcePage({
required this.resourceViewState,
required this.navigatorKey,
});
final ResourceViewState resourceViewState;
final GlobalKey<NavigatorState> navigatorKey;
#override
_ResourcePageState createState() => _ResourcePageState();
}
class _ResourcePageState extends State<DevicePage> {
late DeviceDelegate _routerDelegate;
late ChildBackButtonDispatcher _backButtonDispatcher;
#override
void didChangeDependencies() {
super.didChangeDependencies();
// Defer back button dispatching to the child router
_routerDelegate = InnerRouterDelegate(parentNavigatorKey: widget.navigatorKey, resourceViewState: widget.resourceViewState);
_backButtonDispatcher = Router.of(context).backButtonDispatcher!.createChildBackButtonDispatcher();
_backButtonDispatcher.takePriority();
}
#override
void didUpdateWidget(covariant DevicePage oldWidget) {
super.didUpdateWidget(oldWidget);
_routerDelegate.state = widget.resourceViewState;
}
#override
Widget build(BuildContext context) {
return Router(
routerDelegate: _routerDelegate,
backButtonDispatcher: _backButtonDispatcher,
);
}
}
// InnerRouterDelegate Snippets
// Constructor
InnerRouterDelegate({
required this.resourceViewState,
required this.parentNavigatorKey,
}) {
resourceViewState.addListener(notifyListeners); // See Nested Navigation Example
}
// On Pop Page
onPopPage: (route, result) {
if (result == "return") {
parentNavigatorKey.currentState!.pop();
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},
Question regarding navigating between tabs using indexed stack to display relevant page. I'm doing this in order to keep scroll/state of pages. This works fine. I can change the current page displayed by clicking tab - and can also navigate inside each page (each page is wrapped with it's own Navigator). This is the code for rendering the pages.
Widget build(BuildContext context) {
return IndexedStack(
index: widget.selectedIndex,
children: List.generate(widget._size, (index) {
return _buildNavigator(index);
}));
}
Mu problem is that IndexedStack builds all pages at once. In some of my pages I want to load data from an API, I want to do it when the widget first time built and only if the page is currently visible. Is there a way to do so? in my current implementation all widgets build at once and so all my API calls are called even for the pages that are not currently painted.
Not sure if i'm missing something here, or there is a better way to implement bottom navigation bar. BTW i'm also using Provider for state management.
#tsahnar yea i have also faced same issue related with api call indexed widget render all widgets provided it to its children at once so when individual pages are independently fetching data from api then here comes the problem
try this :
create list of widgets which navigates through your navbar (each widget with key constructor where define PageStorageKey(<key>) for each widgets)
var widgetList = <Widget>[
Page01(key:PageStorageKey(<key>)),
Page02(key:PageStorageKey(<key>))
];
then create PageStorageBucket() which stores your widgets state and provides it in future whenever we need it in a lifetime of app even the widget gets disposed from the tree
final _bucket = PageStorageBucket();
then
var currentIndex = 0;
then in your main base page where the bottom navbar exists in your body instead of IndexedStack use body:PageStorage(bucket: _bucket,child:widgetsList[currentIndex])
and create bottomnavbar in that main base page and then onNavbar icon tab manage index page impherial state by setState((){}) the current state to the currentIndex
it should fix your problem tho its too late after a year
I encountered the same problem. My solution was to save a list of the loaded tabs and then use that to build the list of IndexedStack children inside the Widget build(BuildContext context) method. Then in the onTap method of the BottomNavigationBar, I called setState() to update the list of loaded tabs as well as the current index variable. See below:
class Index extends StatefulWidget {
const Index({Key? key}) : super(key: key);
#override
_IndexState createState() => _IndexState();
}
class _IndexState extends State<Index> {
int _currentIndex = 0;
List loadedPages = [0,];
#override
Widget build(BuildContext context) {
var screens = [
const FirstTab(),
loadedPages.contains(1) ? const SecondTab() : Container(),
loadedPages.contains(2) ? const ThirdTab() : Container(),
loadedPages.contains(3) ? const FourthTab() : Container(),
];
return Scaffold(
appBar: AppBar(
// AppBar
),
body: IndexedStack(
index: _currentIndex,
children: screens,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
var pages = loadedPages;
if (!pages.contains(index)) {
pages.add(index);
}
setState(() {
_currentIndex = index;
loadedPages = pages;
});
},
items: const [
// items
],
),
);
}
}
Now, the API calls on the second, third, and fourth tabs don't call until navigated to.
do you found a solution?
I found the same problem as you and I tried this workaround (i didn't found any issues with it yet)
The idea is to make a new widget to control the visibility state of the widgets that made the api call and build it when it became visible.
In your IndexedStack wrap your _buildNavigator with a widget like this:
class BaseTabPage extends StatefulWidget {
final bool isVisible;
final Widget child;
BaseTabPage({Key key, this.child, this.isVisible});
#override
State<StatefulWidget> createState() => _BaseTabPageState();
}
/*
This state is to prevent tab pages creation before show them. It'll only add the
child widget to the widget tree when isVisible is true at least one time
i.e. if the child widget makes an api call, it'll only do when isVisible is true
for the first time
*/
class _BaseTabPageState extends State<BaseTabPage> {
bool alreadyShowed = false;
#override
Widget build(BuildContext context) {
alreadyShowed = widget.isVisible ? true : alreadyShowed;
return alreadyShowed ? widget.child : Container();
}
}
For example in my code i have something like this to build the navigators for each tab, where _selectedIndex is the selected position of the BottomNavigationBar and tabPosition is the position of that page in the BottomNavigationBar
Widget _buildTabPage(int tabPosition) {
final visibility = _selectedIndex == tabPosition;
return BaseTabPage(
isVisible: visibility,
child: _buildNavigator(tabPosition),
);
}
With this i have the logic of the api call entirely in the children widgets and the bottom navigation knows nothing about them.
Let me know if you see something wrong with it since i'm kind of new with flutter.
Use a PageView instead of an IndexedStack
PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
children: const [
Page1(),
Page2(),
Page3(),
],
),
you can switch pages using the pageController
_pageController.jumpToPage(0);
Flutter will build all widgets inside a stack so if your pages do an API call inside initState, then it will be triggered on build.
What you can do is have a separate function for the API call. Then call the function from the navigator or from the state management.
I hope this helps give you an idea on how to implement this.
I have to IconButton in AppBar and on the press I want to change the font size using Providers Package.
But I am constantly getting this error message:
Error: Could not find the correct Provider above this
HomePage Widget
This likely happens because you used a BuildContext that does not
include the provider of your choice. There are a few common scenarios:
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route,
then other routes will not be able to access that provider.
HomePage.dart
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<FontSizeHandler>(
create: (BuildContext context) => FontSizeHandler(),
child: Scaffold(
appBar: AppBar(
actions: <Widget>[
IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: () {
Provider.of<FontSizeHandler>(context, listen: false)
.increaseFont();
},
),
],
),
body: Consumer<FontSizeHandler>(builder: (context, myFontHandler, _) {
return Container(
child: AutoSizeText(
kDummy,
style: TextStyle(fontSize: myFontHanlder.fontSize),
),
);
}),
),
);
}
}
FontChangeHandler.dart
class FontSizeHandler extends ChangeNotifier {
double fontSize = 15;
void increaseFont() {
fontSize = fontSize + 2;
notifyListeners();
}
void decreaseFont() {
fontSize = fontSize - 2;
notifyListeners();
}
}
The problem is that you are trying to access information you are creating on the same build method.
Before you "Consume" the provider, you need to build a Widget to make sure the creation of your provider took place.
If you dont want to create a new StateLess/StateFull Widget, add a Builder like this:
...body: Builder(
builder:(BuildContext context)=> Consumer<FontSizeHandler>(...))
This way, Builder will make sure that your parent provider gets build before consuming it.
EDIT:
The answer above will do if you want the Provider to be Consumable in the same Stateful/Stateless Widget.
If you need the Provider to be accessed from anywhere in your Flutter App, make sure that you create the Provider before the MaterialApp/CupertinoApp.
Had a similar issue. Solved by importing the provider from the file tree and not the app package.
I was doing this:
import '../fav_provider.dart';
Instead of this.
import 'package:gmartapp/fav_provider.dart';
Just make sure your provider is imported from your app package not your file tree.
My implementation:
main.dart: - it builds multi scaffold (Bottom navigation bar is being created on all screens above UI tree)
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: "WP-seven",
builder: (context, child) {
return Scaffold(
bottomNavigationBar: NavyBottomBar(navigator: (child.key as GlobalKey<NavigatorState>)),
body: child,
);
},
home: NewsList(0),
));
NavyBottomBar.dart: separated class for the navBar widget.
Here we have a Global navigator key that is used in main.dart to connect to every child's navigator(child is every screen widget.)
final GlobalKey<NavigatorState> navigator;
So there is also a logic to open pages, but the code above is enough to show the bottomNavigationBar on every screen and to be able to navigate.
The problem is that I don't need this bottom navigation on every screen, there should be a way to switch off the navigationBar on some screens.
Probably there is a different approach to achieve this result..?
I've found a solution and it works great, but first a few words about the above:
Hero is not an option, because it does not support all types of navigation, like pushReplacement for example and I had a bug with animation in my NavigationBar while using it, probably because hero has built-in animation, too.
Solution:
I've created a new screen called homePage (something like a hub for navigation).
There we have a thing called PageStorageBucket which is useful for storing per-page state that persists across navigations from one page to another. enter link description here
homePage.dart:
Widget newsList;
Widget favorites;
Widget profile;
Widget answers;
List<Widget> pages;
Widget currentPage;
final PageStorageBucket bucket = PageStorageBucket();
#override
void initState() {
newsList = NewsList(isFavorite: 0);
favorites = NewsList(isFavorite: 1);
profile = Profile(userID: widget.userID);
answers = Answers();
pages = [newsList, favorites, profile, answers];
currentPage = newsList;
super.initState();
}
So we've added a number of Widgets(Screens) to a PageStorage bucket and then we use it in Scaffold of homePage.. there is even a place for it.
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageStorage(
child: currentPage,
bucket: bucket,
),
bottomNavigationBar: BottomNavyBar(
currentIndex: currentTab,
onItemSelected: (int index) async {
setState(() {
currentTab = index;
currentPage = pages[index];
});
},
items: [
BottomNavyBarItem(
icon: Icon(Icons.menu),
title: Text('Новости'),
activeColor: Colors.blue,
inactiveColor: Colors.black
),
BottomNavyBarItem(
icon: Icon(Icons.favorite_border),
title: Text('Избранное'),
activeColor: Colors.red,
inactiveColor: Colors.black
),
],
),
);
}
}
It works perfectly.
homePage Scaffold is persistent and does not re-render when redirecting to another page, so we can use nav bars with animations and anything else.
We can choose what pages will be included into this scope.
We can still use Navigator.push and else inside of these screens
It is possible to use multi-scaffold like when you need different appBars, just delete appBar of homePage and it will use an appBar from the opened screen.
The simplest answer is that you should simply be building the bottom navigation bar on each individual page. The navigation bar itself can be put into its own Stateless or StatefulWidget so that you don't have to specify it completely each time. If you want it to look like it's persisting between pages you could probably use flutter's Hero functionality to do so.
If this isn't something you're willing to do, you could use a NavigatorObserver to toggle whether the bottomNavigationBar is shown or not. That'll also mean putting the MaterialApp/Scaffold/etc within a StatelessWidget.