What is the best way to do the bottom navigation bar in flutter?
According to Getting to the Bottom of Navigation in Flutter the flutter team used IndexedStack with Offstage to show the widget when the user changes the tab, but I see there's another way of doing this by TabBarView to change between widgets with simple slide animation and also keeping each widget's scroll state
So what's the difference between the IndexedStack + Offstage and TabBarView? and what is the best way to change the current tab should I use something like flutter_bloc or just use setState()?
OverView
Well, there are many ways to implement BottomNavigationBar in Flutter.
But Using the IndexedStack method would create all the screens of theBottomNavigationBar at the starting. This can be fixed using TabBarView.
Here's how I Implemented BottomNavigationBar in my app using CupertinoTabBar and PageView as it would make only one screen at the starting. And also using AutomaticKeepAliveMixin as it wouldn't let the screens to be recreated again.
KeyPoints
PageView with PageController by which you can easily shift between screens.
AutomaticKeepAliveClientMixin doesn't let the screens to be recreated and thus there is no need to use IndexedStack.
Use Provider and Consumer to recreate only the CupertinoTabBar when changing the currentIndex. Instead of using setState(), as it would recreate the whole screen letting all the widgets to get rebuild. But here were are using Provider to recreate only TabBar.
Code Example
HomePage (BottomNavigtionBar)
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
PageController _pageController;
#override
void initState() {
_pageController = PageController();
super.initState();
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
// Wrapping the CupertinoTabBar in Consumer so that It only get
// recreated.
bottomNavigationBar: Consumer<HomeVM>(
builder: (context, model, child) {
return CupertinoTabBar(
backgroundColor: Colors.white10,
currentIndex: model.currentPage,
onTap: (index) {
index == model.currentPage
? print('same screen')
: _pageController.jumpToPage(
index,
);
model.changePage(index);
},
items: bottomNavItems);
},
),
body:ChangeNotifierProvider(
create: (_) => locator<HelpVM>(),
child: SafeArea(
top: false,
child: PageView(
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: <Widget>[
FrontScreen(),
WorkRootScreen(),
HelpScreen(),
AccountScreen(),
],
),
),
),
);
}
const List<BottomNavigationBarItem> bottomNavItems =
<BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: const Icon(
FontAwesomeIcons.home,
),
),
//...... bottomNavigationBarItems
];
}
HomeVM (Using Provider to change the index and recreate only TabBar using Consumer)
class HomeVM extends ChangeNotifier {
int _currentPage = 0;
int get currentPage => _currentPage;
void changePage(int index) {
this._currentPage = index;
notifyListeners();
}
}
FrontScreen (Here we are using AutomaticKeepAliveClientMixin to retain the state by not recreating the Widget)
class FrontScreen extends StatefulWidget {
#override
_FrontScreenState createState() => _FrontScreenState();
}
class _FrontScreenState extends State<FrontScreen>
with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
// VIMP to Add this Line.
super.build(context);
return SafeArea(
// Your Screen Code
);
}
#override
bool get wantKeepAlive => true;
}
Related
I am currently trying to execute a FutureBuilder future function in an Autorouter - the library (https://pub.dev/packages/auto_route#tab-navigation) - and it works perfectly. However, as I am using a FutureBuilder in the tabs, the future is only executed once - the first time I access the tab - and isn't re-executed again when I leave the tab and come back to it. I would like to be able to execute the future function every time I access the tab since the future is reading data from the database.
I have tried the following:
making the widget stateful and executing setState function to force a rebuild
using the overridden function didChangeDependencies
override the deactivate function of the widget
None of the above seem to work.
And after going through the documentation of the Autoroute library, I haven't come across any explanation on how to force a rebuild of the current tab.
I welcome any suggestions.
Thank you
NB: I'm using Flutter to make a mobile application, the solution doesn't necessarily have to work on a web application.
Tab View
class MyTabView extends StatelessWidget {
MyTabView({Key? key}) : super(key: key);
final tabRoutes = [
TabRoute1(),
TabRoute2(),
];
#override
Widget build(BuildContext context) {
return AutoTabsScaffold(
routes: tabRoutes,
bottomNavigationBuilder: (_, tabRouter) {
return BottomNavigationBar(
currentIndex: tabRouter.activeIndex,
onTap: tabRouter.setActiveIndex,
items: [
BottomNavigationBarItem(
icon: BaseIcon(
svgFileName: 'calendar.svg',
),
label: LocaleKeys.careProfessionalLabelProfile.tr(),
),
BottomNavigationBarItem(
icon: BaseIcon(
svgFileName: 'wallet.svg',
),
label: LocaleKeys.careProfessionalLabelChat.tr(),
),
],
);
},
);
}
}
Tab with child that contains FutureBuilder
class TabRoute2 extends StatefulWidget {
const TabRoute2({Key? key}) : super(key: key);
#override
State<TabRoute2> createState() => _TabRoute2State();
}
class _TabRoute2State extends State<TabRoute2> {
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
// ---- END SPACER
Expanded(
child: ShowFutureData(),
),
],
);
}
}
ShowFutureData
class ShowFutureData extends StatefulWidget {
const ShowFutureData({
super.key,
});
#override
State<ShowFutureData> createState() =>
_ShowFutureDataState();
}
class _ShowFutureDataState extends State<ShowFutureData> {
late FutureDataObjectProvider futureObjectProvider;
#override
initState() {
super.initState();
futureObjectProvider = context.read<FutureDataObjectProvider>();
}
#override
Widget build(BuildContext context) {
retrieved = futureObjectProvider.retrieveAllData();
return FutureBuilder(
future: retrieved, // only executed when the tab is first accessed
initialData: const [],
builder: (context, snapshot) {
// do something with the data
},
);
}
}
You can reassign the future to recall the future.
FutureBuilder(
future: myFuture,
Then reassign it again
myFuture = getData();
I have tackeled without any success with problem...
My main page is stateful class with tabbar with two tabs. First tab has some text from global variables and couple of textfields that also are prefilled with global variables.
Second tab has a button and ontap it calls setstate that changes variables, that are used on first tab and then animates to first tab.
My problem is that first tabs text doesnt change to new value. At the same time textfields will have new values. If i add print command before returning text on first tab, code will print out new values, but state for text is not set, at the same time textfields state will be set.
Its not possible at moment to add code, but i hope i described mu problem good enough.
Thank You!
I tryed many things and now i got strange working solution that makes what i want.
If i just set new variables and after that let tabcontroller to animate dirst page, pages state will not be set, but if i add small delay, then it works like i want. If anyone could explain why, i would be really thankful.
onPressed: () {
setProduct();
Timer(Duration(milliseconds: 100), animateToFirstPage);
}
There is a really elaborate explanation in this answer.
Bottom line, there is a race condition between setState and animateTo, and he suggests breaking it so:
onPressed: () {
setProduct();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
animateToFirstPage;
})
}
Verified it worked for me, and without creepy .sleep solutions
Use a simple state management solution. Where both tabs can listen and modify the values you want. Without code is hard to demonstrate. But you can't simply change the state of a widget from another widget, using provider would be easier.
To update and listen to the change, use StreamController and StreamBuilder. You can put the first tab in a widget combined with SingleTickerProviderStateMixin to prevent it from reloading as well. I created a simple app for demonstration:
Full example:
main.dart
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
final StreamController _streamController = StreamController<String>();
TabController _tabController;
#override
void initState() {
_tabController = TabController(length: 2, vsync: this);
super.initState();
}
#override
void dispose() {
_streamController.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
bottomNavigationBar: TabBar(
controller: _tabController,
labelColor: Colors.red,
tabs: [Tab(text: 'Tab 1'), Tab(text: 'Tab 2')],
),
body: TabBarView(
controller: _tabController,
children: [
FirstTab(_streamController),
Container(
child: Center(
child: TextButton(
child: Text('Press me'),
onPressed: () {
final _someText =
'Random number: ' + Random().nextInt(100).toString();
_streamController.add(_someText);
_tabController.animateTo(0);
},
),
),
),
],
),
);
}
}
class FirstTab extends StatefulWidget {
final StreamController _streamController;
FirstTab(this._streamController);
#override
_FirstTabState createState() => _FirstTabState();
}
class _FirstTabState extends State<FirstTab>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
child: Center(
child: StreamBuilder<String>(
initialData: 'Empty text',
stream: widget._streamController.stream,
builder: (context, snapshot) {
return Text(snapshot.data);
}),
),
);
}
}
let's say I have an app with the following setup:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: MainLoginScreen()),
],
),
));
}
}
I would like to know how can I navigate only the MainLoginScreen widget from the MainMenu with any .push() method.
(I found a way to navigate from a context inside the mainloginscreen,by wrapping it with a MaterialApp widget, but what if I want to use the MainMenu widget instead, which has another context)
There is a general agreement that a 'screen' is a topmost widget in the route. An instance of 'screen' is what you pass to Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen()). So if it is under Scaffold, it is not a screen. That said, here are the options:
1. If you want to use navigation with 'back' button
Use different screens. To avoid code duplication, create MenuAndContentScreen class:
class MenuAndContentScreen extends StatelessWidget {
final Widget child;
MenuAndContentScreen({
required this.child,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: child),
],
),
),
);
}
}
Then for each screen create a pair of a screen and a nested widget:
class MainLoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MenuAndContentScreen(
child: MainLoginWidget(),
);
}
}
class MainLoginWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Here goes the screen content.
}
}
2. If you do not need navigation with 'back' button
You may use IndexedStack widget. It can contain multiple widgets with only one visible at a time.
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState(
initialContentIndex: 0,
);
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
int _index;
_MainMenuAndContentScreenState({
required int initialContentIndex,
}) : _contentIndex = initialContentIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
// A callback that will be triggered somewhere down the menu
// when an item is tapped.
setContentIndex: _setContentIndex,
),
Expanded(
child: IndexedStack(
index: _contentIndex,
children: [
MainLoginWidget(),
SomeOtherContentWidget(),
],
),
),
],
),
),
);
}
void _setContentIndex(int index) {
setState(() {
_contentIndex = index;
});
}
}
The first way is generally preferred as it is declrative which is a major idea in Flutter. When you have the entire widget tree statically declared, less things can go wrong and need to be tracked. Once you feel it, it really is a pleasure. And if you want to avoid back navigation, use replacement as ahmetakil has suggested in a comment: Navigator.of(context).pushReplacement(...)
The second way is mostly used when MainMenu needs to hold some state that needs to be preserved between views so we choose to have one screen with interchangeable content.
3. Using a nested Navigator widget
As you specifically asked about a nested Navigator widget, you may use it instead of IndexedStack:
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
navigatorKey: _navigatorKey,
),
Expanded(
child: Navigator(
key: _navigatorKey,
onGenerateRoute: ...
),
),
],
),
),
);
}
}
// Then somewhere in MainMenu:
final anotherContext = navigatorKey.currentContext;
Navigator.of(anotherContext).push(...);
This should do the trick, however it is a bad practice because:
MainMenu knows that a particular Navigator exists and it should interact with it. It is better to either abstract this knowledge with a callback as in (2) or do not use a specific navigator as in (1). Flutter is really about passing information down the tree and not up.
At some point you would like to highlight the active item in MainMenu, but it is hard for MainMenu to know which widget is currently in the Navigator. This would add yet another non-down interaction.
For such interaction there is BLoC pattern
In Flutter, BLoC stands for Business Logic Component. In its simpliest form it is a plain object that is created in the parent widget and then passed down to MainMenu and Navigator, these widgets may then send events through it and listen on it.
class CurrentPageBloc {
// int is an example. You may use String, enum or whatever
// to identify pages.
final _outCurrentPageController = BehaviorSubject<int>();
Stream<int> _outCurrentPage => _outCurrentPageController.stream;
void setCurrentPage(int page) {
_outCurrentPageController.sink.add(page);
}
void dispose() {
_outCurrentPageController.close();
}
}
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _currentPageBloc = CurrentPageBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
currentPageBloc: _currentPageBloc,
),
Expanded(
child: ContentWidget(
currentPageBloc: _currentPageBloc,
onGenerateRoute: ...
),
),
],
),
),
);
}
#override
void dispose() {
_currentPageBloc.dispose();
}
}
// Then in MainMenu:
currentPageBloc.setCurrentPage(1);
// Then in ContentWidget's state:
final _navigatorKey = GlobalKey();
late final StreamSubscription _subscription;
#override
void initState() {
super.initState();
_subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
}
#override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
// Everything else.
);
}
void _setCurrentPage(int currentPage) {
// Can't use this.context, because the Navigator's context is down the tree.
final anotherContext = navigatorKey?.currentContext;
if (anotherContext != null) { // null if the event is emitted before the first build.
Navigator.of(anotherContext).push(...); // Use currentPage
}
}
#override
void dispose() {
_subscription.cancel();
}
This has advantages:
MainMenu does not know who will receive the event, if anybody.
Any number of listeners may listen on such events.
However, there is still a fundamental flaw with Navigator. It can be navigated without MainMenu knowledge using 'back' button or by its internal widgets. So there is no single variable that knows which page is showing now. To highlight the active menu item, you would query the Navigator's stack which eliminates the benefits of BLoC.
For all these reasons I still suggest one of the first two solutions.
I need help calling a method of another class in a pageview. I have an app which has a pageview consisting of two classes, Page 1 and Page 2 respectively. Both of them are stateful widgets.
I am trying to call a method from Page2 from Page1, but it is difficult for me as both of them do not have a child/parent relationship so I could not use a callback function. In fact, both of them are a child of another parent class (MainParent). I tried using globalkey but i received currentState is null error.
Your help is greatly appreciated. Thanks!
class MainParent extends StatefulWidget{
#override
MainParentState createState() => MainParentSate();
}
class MainParentState extends State<MainParent> {
PageController pageController;
#override
initState(){
pageController = new PageController(
}
List<Widget> _showPageList(AppModel appModel){
List<Widget> pageList = new List();
pageList.add(Page1(/*some named parameters pass here*/));
pageList.add(Page2(/*some named parameters pass here*/));
return pageList;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
physics: new NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: _onPageChanged,
children: _showPageList(),
)
);
}
}
Page 1 class
class Page1 extends StatefulWidget{
#override
Page1State createState() => Page1State();
}
class Page1State extends State<Page1> {
_callPage2Function(){
//animate to page 2 and mount it
MainParent.of(context).pageController.animateToPage(
1,
duration: const Duration(milliseconds: 700),
curve: Curves.fastLinearToSlowEaseIn);
//this doesn't work
Page2State page2state = new Page2State();
page2state.refreshList();
//creating a global key doesnt work as well, gets currentstate is null
GlobalKey<Page2State> page2Key = new GlobalKey<Page2State>();
page2Key.currentState.refreshList();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: GestureDetector(
onTap: () => _callPage2Function()
)
)
);
}
}
Page 2 class
class Page2 extends StatefulWidget{
#override
Page2State createState() => Page2State();
}
class Page2State extends State<Page2> {
refreshList(){
//this function refreshes the list
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: ListView.builder(
//some child widgets here
)
)
);
}
}
Edit: Edited my code. Turns out I have to navigate to page2 tab first, because when I am in page 1, page 2 is not mounted to the screen. So i added a pageController and navigate to page2 first.
The error in your code is that you are not injecting the key in you Page2 instance when using it, thats why the currentState in null.
Usually when i want to achieve this, i use the key attribute like so
class Page2 extends StatefulWidget {
static final GlobalKey<Page2State> staticGlobalKey =
new GlobalKey<Page2State>();
Page2() : super(key: Page2.currentStateKey); // Key injection
#override
Page2State createState() => Page2State();
}
class Page2State extends State<Page2> {
refreshList() {
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: ListView.builder(
//some child widgets here
)));
}
}
then to call the refreshList() from anywhere in your app just do
Page2.staticGlobalKey.currentState.refreshList();
You could work around and use another approach instead of static key, but this should be a working solution.
In PageView, Only one page is mounted(or active) at a time.
Here when you call page2Key.currentState.refreshList(); from Page1, the Page2 is currently unmounted(or deactive). So the state itself is null.
So you cannot do setState of another page.
I have a Navbar state full widget that tracks current page and returns a widget with a bottom navbar and dynamic body based of current page which is stored as a state
class _PullingoNavbarState extends State<PullingoNavbar> {
static int _page = 1;
final _screens = {0: PullingoMap(), 1: Dashboard(), 2: Dashboard()};
#override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_page],
bottomNavigationBar: CurvedNavigationBar(
animationDuration: Duration(milliseconds: 200),
backgroundColor: Colors.blueAccent,
index: _page,
items: <Widget>[
PullingoIcon(icon: Icons.favorite),
PullingoIcon(icon: Icons.chrome_reader_mode),
PullingoIcon(icon: Icons.person),
],
onTap: (index) {
setState(() {
_page = index;
});
},
),
);
}
}
and the root widget as follows:
class RoutesWidget extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(
title: 'PULLINGO',
theme: pullingoTheme,
routes: {
"/": (_) => PullingoNavbar(),
},
);
}
pre creating instances of _screens in a map doesn't feel like a good approach to me. this probably will create states of those screens regardless of user visits them or not. There are few suggestions given here. does the above method look okay or should I completely avoid this approach.
You can use PageView and AutomaticKeepAliveClientMixin to persist your widgets when navigating. With this approach a widget is only created when a user navigates to it by bottom navigation bar. I have recently written an article about how to use it, might be useful.
https://cantaspinar.com/persistent-bottom-navigation-bar-in-flutter/