How to persist tabs when using BottomNavigationBar? - flutter

Whenever I switch from Tab 1 to Tab 2 then back to Tab 1, content on Tab 1 is rebuilt. Anyway to prevent this?

class BottomNavBarExample extends StatefulWidget {
#override
BottomNavBarExampleState createState() {
return new BottomNavBarExampleState();
}
}
class BottomNavBarExampleState extends State<BottomNavBarExample> {
List<BottomNavigationBarItem> _tabs = [
BottomNavigationBarItem(
icon: Icon(Icons.library_books), title: Text('Library')),
BottomNavigationBarItem(icon: Icon(Icons.public), title: Text('Public')),
BottomNavigationBarItem(icon: Icon(Icons.person), title: Text('Account')),
];
var _currentIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Demo')),
bottomNavigationBar: BottomNavigationBar(
currentIndex: this._currentIndex,
items: this._tabs,
onTap: (value) {
setState(() {
this._currentIndex = value;
});
},
),
body: Stack(children: [
new Offstage(
offstage: this._currentIndex != 0,
child: new TickerMode(
enabled: this._currentIndex == 0,
child: LibraryPage(),
),
),
new Offstage(
offstage: this._currentIndex != 1,
child: new TickerMode(
enabled: this._currentIndex == 1,
child: GalleryPage(),
),
),
new Offstage(
offstage: this._currentIndex != 2,
child: new TickerMode(
enabled: this._currentIndex == 2,
child: AccountPage(),
),
),
]),
);
}
}
Then for each page, include AutomaticKeepAliveClientMixin

Related

How to keep drawer always open

I want to put a drawer like this in my Flutter app:
just like https://m3.material.io/develop/flutter
I'm using NavigationRail and it's said that a menu button can be added to open a navigation drawer. Does any knows how to add the menu button and the drawer?
menu button of NavigationRail
thanks.
It's a bit hard to use a regular Scaffold Drawer without the regular scaffold controls, as far as I can tell.
I came up with a solution for your problem, if I understood it correctly. Looks a lot like the spec site, needs a bit of styling.
Took the example from the NavigationRail documentation and added a Visibility widget. Now clicking on the destinations, you can show and hide their child widgets(drawer). No drawer animation though.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: const NavRailExample(),
);
}
}
class NavRailExample extends StatefulWidget {
const NavRailExample({super.key});
#override
State<NavRailExample> createState() => _NavRailExampleState();
}
class _NavRailExampleState extends State<NavRailExample> {
int _selectedIndex = 0;
NavigationRailLabelType labelType = NavigationRailLabelType.all;
bool showLeading = false;
bool showTrailing = false;
double groupAligment = -1.0;
bool _isClosed = false;
Widget _getWidget(int index) {
switch (index) {
case 1:
return GestureDetector(
child: const Text('Tap!'),
onTap: () => setState(() {
_isClosed = true;
}),
);
case 2:
return const Text('empty');
default:
return ListView(
children: const [
ExpansionTile(
title: Text('whatev'),
children: [Text('1'), Text('2')],
),
ListTile(
title: Text('adfafdafaf'),
)
],
);
}
}
Widget _getPage(int index) {
switch (index) {
case 1:
return const Center(child: Text('sheeesh'));
case 2:
return const Center(child: Text('empty'));
default:
return const Center(child: Text('yolo'),);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Row(
children: <Widget>[
NavigationRail(
selectedIndex: _selectedIndex,
groupAlignment: groupAligment,
onDestinationSelected: (int index) {
setState(() {
_isClosed = (_selectedIndex == index || _isClosed)
? !_isClosed
: _isClosed;
_selectedIndex = index;
});
},
labelType: labelType,
leading: showLeading
? FloatingActionButton(
elevation: 0,
onPressed: () {
// Add your onPressed code here!
},
child: const Icon(Icons.add),
)
: const SizedBox(),
trailing: showTrailing
? IconButton(
onPressed: () {
// Add your onPressed code here!
},
icon: const Icon(Icons.more_horiz_rounded),
)
: const SizedBox(),
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('First'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.book),
label: Text('Second'),
),
NavigationRailDestination(
icon: Icon(Icons.star_border),
selectedIcon: Icon(Icons.star),
label: Text('Third'),
),
],
),
Visibility(
maintainState: false,
visible: !_isClosed,
child: Row(
children: [
const VerticalDivider(thickness: 1, width: 1),
SizedBox(
height: double.infinity,
width: 200,
child: _getWidget(_selectedIndex),
)
],
),
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: _getPage(_selectedIndex),
),
],
),
),
);
}
}

Why FutureBuilder re-build every time?

I'm working with FutureBuilder and I initialize _future in InitState as mentioned here.
Despite this, FutureBuilder re-build itself every time I switch page with the BottomNavigationBar.
Code Sample:
class _HomeViewState extends State<HomeView> {
late final Future<List<DocumentSnapshot<Object?>>> _futureSerieA;
#override
void initState() {
super.initState();
_futureSerieA = getScheduled("Serie A");
}
#override
Widget build(BuildContext context) {
return SizedBox.expand(
child: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(0,0,0,0),
child: Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.transparent)
)
),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: FutureBuilder(
future: _futureSerieA,
builder: (context, AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
if (snapshot.hasData) {
List<String> scheduled = [];
for (var DOC in snapshot.data!) {
scheduled.add(DOC.id);
}
return ...
How could I disable FutureBuilder re-build when browsing between pages of BottomNavigationBar?
BottomNavBar:
class _LoggedHandleState extends State<LoggedHandle> {
);
double height = AppBar().preferredSize.height;
int _selectedPage = 1;
final _pageOptions = [
const BetView(),
const HomeView(),
const UserView()
];
#override
Widget build(BuildContext context) {
return Scaffold(
...
),
bottomNavigationBar: BottomNavigationBar(
unselectedItemColor: Colors.white60,
backgroundColor: Colors.red,
selectedItemColor: Colors.white,
currentIndex: _selectedPage,
onTap: (int index) {
setState(() {
_selectedPage = index;
});
},
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.receipt),
label: 'Schedina',
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.account_circle),
label: 'Account',
),
]),
body: _pageOptions[_selectedPage],
);
}
}
When browsing between pages of bottom navigation bar, your state is not maintained. This behaviour causes the widget to rebuild every time.
You can use Indexed Stack https://api.flutter.dev/flutter/widgets/IndexedStack-class.html
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
current_tab = index;
});
},
currentIndex: current_tab,
items: [
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
),
],
),
body: IndexedStack(
children: <Widget>[
PageOne(),
PageTwo(),
],
index: current_tab,
),
);
}
Although this is the best solution, it will load all your widgets once the IndexedStack is loaded.
I found a Lazy Loading Indexed Stack util to load your widgets when and only the first time they are created https://github.com/okaryo/lazy_load_indexed_stack

Create two custom buttons inside Bottom Navigation Bar to control four pages using page view

I want to control four pages using two arrows on the bottom navigation bar, and in the middle of these two arrow buttons I have a counter to show the ID of the page. Like the image below:
I'm using modular route to change the pages, but I don't know how can I do that using only two buttons, and don't have idea how can I put the counter in the middle of this two buttons. Any suggestion?
class _CreateAccountPageState
extends ModularState<CreateAccountPage, CreateAccountController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: controller.pageViewController,
children: [
RouterOutlet(),
RouterOutlet(),
RouterOutlet(),
RouterOutlet()
],
),
bottomNavigationBar: AnimatedBuilder(
animation: controller.pageViewController,
builder: (context, snapshot) {
return BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
elevation: 0,
backgroundColor: Colors.white,
currentIndex: controller.pageViewController.page?.round() ?? 0,
onTap: (id) {
if (id == 0) {
Modular.to.navigate('/createaccount/pageStep1');
} else if (id == 1) {
Modular.to.navigate('/createaccount/pageStep2');
} else if (id == 2) {
Modular.to.navigate('/createaccount/pageStep3');
} else if (id == 3) {
Modular.to.navigate('/createaccount/pageStep4');
}
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Page1',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Page2',
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Page3',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Page4',
)
]
/*ElevatedButton(
onPressed: () => Modular.to.navigate("/login"),
child: Text("Voltar"))*/
);
},
));
}
Here is the output, I hope you can design the rest:
Widget
import 'package:flutter/material.dart';
class CreateAccountPage extends StatefulWidget {
CreateAccountPage({Key? key}) : super(key: key);
#override
_CreateAccountPageState createState() => _CreateAccountPageState();
}
class _CreateAccountPageState extends State<CreateAccountPage> {
PageController controller = PageController(initialPage: 0);
int currentPage = 0;
final pages = List.generate(
4,
(index) => Container(
alignment: Alignment.center,
color: index.isEven ? Colors.cyanAccent : Colors.yellowAccent,
child: Text(
"${index + 1}",
style: TextStyle(fontSize: 44),
),
),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: controller,
children: [...pages],
onPageChanged: (value) {
setState(() {
currentPage = value;
});
},
),
bottomNavigationBar: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () {
setState(() {
controller.previousPage(
duration: Duration(milliseconds: 400),
curve: Curves.easeInOut);
});
},
icon: Icon(Icons.arrow_left),
),
Text(
"${currentPage + 1}/${pages.length}",
),
IconButton(
onPressed: () {
controller.nextPage(
duration: Duration(milliseconds: 400),
curve: Curves.easeInOut);
},
icon: Icon(Icons.arrow_right),
)
],
),
));
}
}
I think what you are looking for is an onboarding screen. I suggest you take a look at this library here which is created especially for onboarding named introduction_screen. you can customize the bottons and texts as shown in the examples. read documentation for more information.

Flutter - appbar bottom property not updating properly with setState

I'm building an app with an appbar that has the bottom property that will only be visible if the user selects the middle item of the 3 items (item index = 1) in the bottom navigation bar. here's the code below.
class _MainPageState extends State<MainPage> {
int index = 2;
TabBar bottomBar = null;
#override
Widget build(BuildContext context) => DefaultTabController(
initialIndex: 0,
length: 3,
child: Scaffold(
appBar: AppBar(title: Text(widget.title), bottom: bottomBar),
bottomNavigationBar: BottomNavigationBar(
currentIndex: index,
selectedItemColor: Colors.teal,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.bolt),
label: ('Devices'),
),
BottomNavigationBarItem(
icon: Icon(Icons.pie_chart),
label: ('Cluster'),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: ('Settings'),
),
],
onTap: (int index) => setState(() => this.index = index),
),
body: buildPages(),
),
);
Widget buildPages() {
// bottomNavBarPageBuilder
switch (index) {
case 0:
setState(() {
this.bottomBar = null;
print(index);
});
return DeviceListPage();
case 1:
setState(() {
print(index);
this.bottomBar = TabBar(
tabs: [
Tab(
icon: Icon(Icons.power),
),
Tab(
icon: Icon(Icons.power),
),
Tab(
icon: Icon(Icons.power),
)
],
);
});
return TabBarView(
children: [UserLocalPage(), Container(), Container()]);
case 2:
setState(() {
print(index);
this.bottomBar = null;
});
return SettingsPage();
default:
setState(() {
print(index);
this.bottomBar = null;
});
return Container();
}
}
}
the problem is that sometimes the setState doesn't update the "bottom" property of the appBar. here's a video demonstrating the problem
how can I make the bottom property of the appBar be updated properly? any help would be much appreciated thankyou 🙏🙏
Sorry for a late answer. Would you kindly try this code for me?
Also, although I didn't change that in the code below, I noticed that your index == 2, but initialIndex == 0. I would recommend creating a const variable to represent initialIndex, like this:
static const int initialIndex = 0;
int index = initialIndex;
//skipped code
initialIndex: initialIndex,
class _MainPageState extends State<MainPage> {
int index = 2;
TabBar bottomBar = null;
#override
Widget build(BuildContext context) => DefaultTabController(
initialIndex: 0,
length: 3,
child: Scaffold(
appBar: AppBar(title: Text(widget.title), bottom: bottomBar),
bottomNavigationBar: BottomNavigationBar(
currentIndex: index,
selectedItemColor: Colors.teal,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.bolt),
label: ('Devices'),
),
BottomNavigationBarItem(
icon: Icon(Icons.pie_chart),
label: ('Cluster'),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: ('Settings'),
),
],
onTap: _onTap,
),
body: buildPages(),
),
);
void _onTap(int index) {
if(index != this.index) {
setState(() {
this.index = index;
switch(index) {
case 1:
bottomBar = TabBar(
tabs: [
Tab(
icon: Icon(Icons.power),
),
Tab(
icon: Icon(Icons.power),
),
Tab(
icon: Icon(Icons.power),
)
],
);
break;
default:
bottomBar = null;
}
});
}
}
Widget buildPages() {
// bottomNavBarPageBuilder
switch (index) {
case 0:
return DeviceListPage();
case 1:
return TabBarView(
children: [UserLocalPage(), Container(), Container()]);
case 2:
return SettingsPage();
default:
return Container();
}
}
}

How to toggle visibility of TabBar with Bottom Navigation Items in Flutter

I have a bottomNavigationBar and an AppBar in my flutter app. At the bottom of the AppBar is a TabBar consisting of two items. So I want the TabBar to be invisible when some items of the BottomNavigationBar is clicked. I tried to assign the Visibility class to my TabBar with a Boolean variable but it doesn't work. It seems like I can't handle the TabBar widget separately.
How do resolve this?
class DashBoardPage extends StatefulWidget {
#override
_DashBoardPageState createState() => _DashBoardPageState();
}
class _DashBoardPageState extends State<DashBoardPage> {
SharedPreferences sharedPreferences;
bool showTabs = false;
int tabsIndex = 0;
int _currentIndex = 0;
String _appBarText = "Welcome, User";
Widget callPage(int currentIndex) {
switch (currentIndex) {
case 0:
showTabs = true;
_appBarText = "Welcome, User";
return TabBarView(
children:[
new HomePage(),
new SchedulePage()
]
);
break;
case 1:
showTabs = false;
break;
case 2:
showTabs = false;
break;
default:
return HomePage();
}
}
#override
void initState() {
super.initState();
checkLoginState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MAF Mentor',
debugShowCheckedModeBanner: false,
home: DefaultTabController(
length: choices.length,
child: Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFFFFFFF),
title: Text(
_appBarText,
style: TextStyle(
color: Color(0xFF1C2447),
fontFamily: 'Muli',
fontSize: 16.0,
),
),
bottom: showTabs? TabBar(
isScrollable: true,
tabs: choices.map<Widget>((Choice choice) {
return Tab(
text: choice.title,
icon: Icon(choice.icon),
);
}).toList(),
labelColor: Color(0xFF1C2447),
):null,
actions: <Widget>[
IconButton(
icon: Icon(
Icons.account_circle,
color: Color(0xFF1C2447),
),
onPressed: () {
Navigator.of(context).pushNamed('/profile_page');
},
),
IconButton(
icon: Icon(
Icons.notifications,
color: Color(0xFF1C2447),
),
onPressed: () {
// do something
},
),
],
), //AppBar
body: callPage(_currentIndex),
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
fixedColor: Color(0xFF1C2447),
currentIndex: _currentIndex,
onTap: (value) {
_currentIndex = value;
callPage(_currentIndex);
setState(() {
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text("Bar 1")),
BottomNavigationBarItem(
icon: Icon(Icons.people), title: Text("Bar 2")),
BottomNavigationBarItem(
icon: Icon(Icons.history), title: Text("Bar 3"))
],
),
),
),
);
}
bottom requires a PreferredSizeWidget so you can not use the Visibility widget there. You can use a boolean variable to do that. You can see the whole code below. Since I don't know your choices and tabs I randomly put something. But the idea is if you want to show TabBar when user tap BottomNavigationBarItem
number 1 you just update your boolean variable as true. Otherwise make it false.
class TabBarExample extends StatefulWidget {
#override
_TabBarExampleState createState() => _TabBarExampleState();
}
class _TabBarExampleState extends State<TabBarExample> {
bool showTabs = false;
int selectedIndex = 0;
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFFFFFFF),
title: Text(
'_appBarText',
style: TextStyle(
color: Color(0xFF1C2447),
fontFamily: 'Muli',
fontSize: 16.0,
),
),
bottom: showTabs
? TabBar(
isScrollable: true,
tabs: <Widget>[
Tab(
text: 'Choice1',
icon: Icon(Icons.add_circle_outline),
),
Tab(
text: 'Choice1',
icon: Icon(Icons.add_circle),
),
],
labelColor: Color(0xFF1C2447),
)
: null,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: selectedIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('first')),
BottomNavigationBarItem(
icon: Icon(Icons.favorite), title: Text('second')),
],
onTap: (index) {
if (index == 1) {
setState(() => showTabs = true);
} else {
setState(() => showTabs = false);
}
setState(() => selectedIndex = index);
},
),
),
);
}
}