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.
Related
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()
),
...
);
},
);
}
...
}
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 have 3 pages (PageA, PageB & PageC) in pageView on the home page. I can switch between pages easily using a PageController. The 3 pages are also connected to BottomNavigationBar. I can also switch between pages via that.
Now, I have a problem:
How can I go to the PageB or PageC page in the PageView, from a page that is outside the home page?
Consider an example:
I go to PageA,
from there I go to a PageQ.
From PageQ, I tap the 2nd BottomNavBarItem for PageB. From here I am stuck.
If I try to navigate to HomePage, the PageA being initial page is displayed on the screen.
return Scaffold(
body: PageView(
controller: controller.pageController,
children: [
PageA(),
PageB(),
PageB(),
],
onPageChanged: controller.slidePage,
),
bottomNavigationBar: BottomNavbar(),
);
if you are trying to achieve a successful bottom navigation bar , you can try something like this :
import 'package:convex_bottom_bar/convex_bottom_bar.dart';
import 'package:flutter/material.dart';
import 'presentation/screens/cart.dart';
import 'presentation/screens/home.dart';
import 'presentation/screens/order.dart';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int page = 0;
final _order = PageA();
final _home = Pageb();
final _cart = Pageq();
Widget _showPage = PageA();
Widget _pageChooser(page) {
switch (page) {
case 0:
return Pageq();
break;
case 1:
return Pageb();
break;
case 2:
return Pageq();
break;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 10.0,
backgroundColor: Colors.pink[100],
title: Image.asset("images/logo.png",scale: 14.0,),
centerTitle: true,
),
bottomNavigationBar: BottomNavigationBar(
elevation: 0.0,
items: [
BottomNavigationBarItem(
label:"PageQ",
icon:Icon(Icons.home)
),
BottomNavigationBarItem(
label:"PageB",
icon:Icon(Icons.ac_unit)
),
BottomNavigationBarItem(
label:"PageA",
icon:Icon(Icons.favorite)
),
],
onTap: (index) async {
setState(() {
_showPage = _pageChooser(index);
});
},
),
body: _showPage,
);
}
}
One way is to pass that controller around which is probably a bad idea. A better way is to use Navigator You can even stick with PageView and pass a value to the constructor to set the initial page, but do think about redesigning. You may not need PageView maybe using separate pages with just Navigator to navigate is better. If I remember correctly page view can be useful if you need sequential steps, but don't limit yourself to it.
This is a logical problem rather than a technical one. Still answering so that anyone who faced such an issue could snap out of it.
PageView class works as intended. When you're navigating from another page that is not present in the PageView use popUntil instead of push. As push would create a new Home Page instead of the existing one!
void redirectTo(int selectedPageIndex) async {
if (Get.currentRoute != Routes.HOME) {
Navigator.popUntil(Get.context, ModalRoute.withName(Routes.HOME));
}
pageController.jumpToPage(selectedPageIndex);
}
Just pop until you're back to the home page and then jump to the desired index!
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.