Why FutureBuilder re-build every time? - flutter

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

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),
),
],
),
),
);
}
}

how to access PageViewController outside of widget

TabScreen is my main widget in my app that includes a BottomNavigationBar and a PageView to transition between pages.
PageController allows me to change views and everything is fine here, but there is one thing missing. How can I change my PageView controller outside of TabScreen? I'd like to put a button to access my main PageView controller from within my pages.
class _TabsScreenState extends State<TabsScreen> {
bool _isloading = true;
int? _selectedIndex;
List<Widget>? _pages = [
HomeScreen(),
CurrenPlanDetail(),
Container(),
ProfileScreen(),
SettingScreens(),
];
PageController? _pageController;
#override
void initState() {
_selectedIndex = 0;
_pageController = PageController(initialPage: _selectedIndex!);
}
}
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
children: _pages!,
physics: NeverScrollableScrollPhysics(),
),
bottomNavigationBar: Visibility(
visible: !_isloading,
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
selectedItemColor: kNewPurple,
//unselectedItemColor: Colors.grey,
currentIndex: _selectedIndex!,
onTap: (value) {
setState(() {
_selectedIndex = value;
_pageController!.jumpToPage(_selectedIndex!);
});
},
backgroundColor: Colors.grey[300],
items: [
BottomNavigationBarItem(
icon: Icon(
Ionicons.home_outline,
size: 15.sp,
),
label: 'Home'),
BottomNavigationBarItem(
icon: Icon(
Ionicons.reader_outline,
size: 15.sp,
),
label: 'Plan'),
BottomNavigationBarItem(
icon: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: CircleBorder(),
fixedSize: Size(50, 50),
),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) {
return FloatingButton();
});
},
child: Icon(Ionicons.add)),
label: ''),
BottomNavigationBarItem(
icon: Icon(
Ionicons.happy_outline,
size: 15.sp,
),
label: 'Profile'),
BottomNavigationBarItem(
icon: Icon(
Ionicons.settings_outline,
size: 15.sp,
),
label: 'Setting'),
],
),
),
);
}
}
for example, somewhere in my app, I'd like to put a button and pass a function like this :
_pageController!.jumpToPage(1);
Define PageController as static and
TabScreen.pageController.jumpToPage(1);

Flutter: Combine Drawer and BottomNav in PageView

I would like to have a bottomNavBar and a Drawer at the same time, which are supposed to lead to different pages. In my PageView there are a few Screens, which are, so I thought, accessible
if I call the pageController's function to change the page with the correct index.
Now the tricky part: I only use 3 out of these 6 in my
BottomNav and they work just fine. But I was thinking that
it has to be possible to access the other ones too,
if I get the correct index, i.e. I want to access the other screens/indices
with the drawer.
However, if I use the _pageController.jumpToPage(int page)
command with any index greater than 2, I get the following error:
'0 <= currentIndex && currentIndex < items.length': is not true.
Any ideas what I am missing?
Edit:
class _MainScreenState extends State<MainScreen> {
var _pageController = PageController();
int _page = 0;
List drawerItems = [
{"icon": Icons.add,"name": "Feed",},
{"icon": Icons.delete, "name": "Your Feed",},
{"icon": Icons.delete, "name": "test1",},
{"icon": Icons.delete, "name": "test2",},
{"icon": Icons.delete, "name": "test3",},
];
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => Dialogs().showExitDialog(context),
child: Scaffold(
body: _buildPageView(),
bottomNavigationBar: _buildBottomNavigation(),
drawer: _buildDrawer(),
),
);
}
PageView _buildPageView(){
return PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
onPageChanged: onPageChanged,
children: const [
Test1Screen(),
Test2Screen(),
Test3Screen(),
Test4Screen(),
Test5Sreen(),
Test6Screen()
],
);
}
void navigationTapped(int page) {
_pageController.jumpToPage(page);
}
#override
void initState() {
super.initState();
_pageController = PageController(initialPage: 0);
}
#override
void dispose() {
super.dispose();
_pageController.dispose();
}
void onPageChanged(int page) {
print(page);
setState(() {
this._page = page;
});
}
BottomNavigationBar _buildBottomNavigation(){
return BottomNavigationBar(
backgroundColor: Theme.of(context).primaryColor,
selectedItemColor: Theme.of(context).accentColor,
unselectedItemColor: Colors.grey[500],
elevation: 20,
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(
FeatherIcons.home,
),
label: "Test1"
),
BottomNavigationBarItem(
icon: Icon(
FeatherIcons.compass,
),
label: "Test2"
),
BottomNavigationBarItem(
icon: Icon(
FeatherIcons.settings,
),
label: "Test3"
),
],
onTap: navigationTapped,
currentIndex: _page,
);
}
Drawer _buildDrawer(){
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
child: const Center(
child: Text("Header Area"),
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor
),
),
ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: drawerItems.length,
itemBuilder: (BuildContext context, int index){
Map item = drawerItems[index];
return _buildDrawerListTile(item,index);
})
],
),
);
}
Widget _buildDrawerListTile(Map item, int index){
return ListTile(
leading: Icon(
item['icon'],
color: _page == index
?Theme.of(context).primaryColorLight
: Theme.of(context).primaryColorDark
),
title: Text(
item["name"],
style: TextStyle(
color: _page == index
?Theme.of(context).primaryColorLight
:Theme.of(context).primaryColorDark
)
),
onTap: (){
navigationTapped;
},
);
}
}

Change tab on BottomNavigationBar Flutter programmatically and without using onTap of BottomNavigationBar?

I am working on a flutter application where I need to redirect to the first screen on BottomNavigationBar when the user presses back from any other screen of the remaining BottomNavigationBar screens. For now, I have added redirecting event on a simple button, will replace this on _onWillPop event.
Please find the code below:
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final PageStorageBucket bucket = PageStorageBucket();
Widget currentScreen = HomeFragment();
int currentTab = 0;
static int selectedIndexN = 0;
static const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
List<Widget> _widgetOptions1 = <Widget>[
HomeFragment(),
LoginFargment(),
SignUpFargment(),
ProfileFargment(),
];
void changeTabMethod(int index) {
print('changeTabMethod is called');
setState(() {
selectedIndexN = index;
});
print('changeTabMethod is called : selectedIndexN : $selectedIndexN');
}
#override
Widget build(BuildContext context) {
return Scaffold(
// return GetBuilder<DashboardController>(
body: Center(
child: _widgetOptions1.elementAt(selectedIndexN),
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: selectedIndexN,
onTap: changeTabMethod,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Login',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'SignUp',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'Profile',
),
],
),
);
}
}
Profile screen code:
class ProfileFargment extends StatefulWidget {
#override
_ProfileFragmentState createState() => _ProfileFragmentState();
}
class _ProfileFragmentState extends State<ProfileFargment> {
#override
Widget build(BuildContext context) {
return Scaffold(
body:SafeArea(
child: Container(
padding: EdgeInsets.all(20.0),
height: double.infinity,
width: double.infinity,
color: Colors.teal,
child: GestureDetector(
onTap: () {
//Calling method changeTabMethod(0)
HomeScreen().createState().changeTabMethod(0);
},
child: Container(
margin: EdgeInsets.only(top: 20.0),
height: 40.0,
width: 150.0,
color: Colors.white,
child: Center(child: Text('Profile'),),
),
),
),
),
);
}
}
On the other hand, when I call changeTabMethod from a ProfileFragment screen, it will enter into changeTabMethod but couldn't execute the setState method. So my tab is not changing.
You can consider this console report:
changeTabMethod is called is only printed the second print after setState was not executed.
Can you please let me know what or where I am doing anything wrong?
Thanks in advance :-)
Try below code. By passing function as parameter you can trigger function on home page from any other page.
Home screen code:
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final PageStorageBucket bucket = PageStorageBucket();
// Widget currentScreen = HomeFragment();
int currentTab = 0;
static int selectedIndexN = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
Widget _widgetOptions1(int index) {
switch (index) {
case 0:
return ProfileFargment(onButtonPressed: changeTabMethod);
case 1:
return Container(child: Text("Page - 2 "));
case 2:
return Container(child: Text("Page - 3 "));
default:
return Container();
}
}
void changeTabMethod(int index) {
print('changeTabMethod is called');
setState(() {
selectedIndexN = index;
});
print('changeTabMethod is called : selectedIndexN : $selectedIndexN');
}
#override
Widget build(BuildContext context) {
return Scaffold(
// return GetBuilder<DashboardController>(
body: Center(
child: _widgetOptions1(selectedIndexN),
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: selectedIndexN,
onTap: changeTabMethod,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Login',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'SignUp',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'Profile',
),
],
),
);
}
}
Profile screen code:
class ProfileFargment extends StatefulWidget {
final void Function(int) onButtonPressed;
const ProfileFargment({Key key, this.onButtonPressed});
#override
_ProfileFragmentState createState() => _ProfileFragmentState();
}
class _ProfileFragmentState extends State<ProfileFargment> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: EdgeInsets.all(20.0),
height: double.infinity,
width: double.infinity,
color: Colors.teal,
child: GestureDetector(
onTap: () {
//Calling method changeTabMethod(0)
// HomeScreen().createState().changeTabMethod(0);
widget.onButtonPressed(0);
},
child: Container(
margin: EdgeInsets.only(top: 20.0),
height: 40.0,
width: 150.0,
color: Colors.white,
child: Center(
child: Text('Profile'),
),
),
),
),
),
);
}
}
This is actually quite simple, you just need to pass the function off to the child widget.
So your ProfileFragment will take a variable called changeTab of type Function(int):
Function(int) changeTab;
ProfileFragment(this.changeType); // Constructor
You pass that off when you create the widget inside the _HomeScreenState:
List<Widget> _widgetOptions1 = <Widget>[
…
ProfileFargment(changeTabMethod),
];
Then you can call the function directly in the _ProfileFragmentState:
onTap: () => widget.changeTab(0);

How to make Navigator stay on the active route when StreamProvider updates?

In the code below I have created a custom Navigator which is a child to a StreamProvider. My problem is that every time a receive a snapshot from Firestore, the StreamProvider resets the Navigator route. How can I keep the current route while still updating the UI with the new values from the stream?
Also, the ListView in the second code block resets the scroll position on updates from the stream. How can I preserve the scroll position, while still updating the UI with new values?
class _HomeState extends State<Home> {
#override
initState() {
super.initState();
}
int _selectedIndex = 0;
Future<void> _onItemTapped(int index) async {
setState(() {
_selectedIndex = index;
print(index);
});
}
#override
Widget build(BuildContext context) {
final FixturesPage fixturesPage = new FixturesPage();
final ProfilePage profilePage = new ProfilePage();
FirebaseUser user = Provider.of<FirebaseUser>(context);
List<Widget> pages = [
fixturesPage,
profilePage,
];
return MultiProvider(
providers: [
StreamProvider<int>.value(
value: DatabaseService(userId: user.uid).totalPoints),
StreamProvider<List<Bet>>.value(
value: BettingService(userId: user.uid).getBetList)
],
child: Scaffold(
appBar: AppBar(
elevation: 0.0, backgroundColor: darkShade5, title: AppBarTitle()),
body: Container(
decoration: BoxDecoration(
color: darkShade4,
),
child: pages[_selectedIndex],
),
bottomNavigationBar: Container(
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: darkShade7,
selectedFontSize: 10,
unselectedFontSize: 10,
elevation: 0,
unselectedIconTheme:
IconThemeData(color: Colors.white.withOpacity(0.9)),
selectedIconTheme: IconThemeData(color: mainColor),
unselectedLabelStyle: navigationBarStyle.copyWith(
color: Colors.white.withOpacity(0.9)),
selectedLabelStyle: navigationBarStyle.copyWith(color: mainColor),
items: [
BottomNavigationBarItem(
icon: Container(
height: 20,
width: 20,
child: Image(
image: AssetImage("lib/images/HomeIcon.png"),
color: _selectedIndex == 0
? mainColor
: Colors.white.withOpacity(0.9),
),
),
title: SizedBox()),
BottomNavigationBarItem(
icon: Container(
height: 20,
width: 20,
child: Image(
image: AssetImage("lib/images/ProfileIcon.png"),
color: _selectedIndex == 1
? mainColor
: Colors.white.withOpacity(0.9),
),
),
title: SizedBox()),
],
currentIndex: _selectedIndex,
selectedItemColor: mainColor,
unselectedItemColor: Colors.white.withOpacity(0.3),
onTap: _onItemTapped,
),
),
),
);
}
}
class _FixturesPageState extends State<FixturesPage> {
int selectedTap = 0;
Function setSelectedTap(int tap) {
setState(() {
selectedTap = tap;
});
return null;
}
Widget selectedSport = FavoritesTap();
Function setSelectedSport(Widget newSelectedSport) {
setState(() {
selectedSport = newSelectedSport;
});
return null;
}
ScrollController listViewController =
ScrollController(keepScrollOffset: true);
#override
Widget build(BuildContext context) {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
List<dynamic> matches = Provider.of<List<SoccerMatch>>(context);
return Navigator(
key: navigatorKey,
initialRoute: "/",
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(
builder: (context) => Container(
color: darkShade6,
child: Column(
children: <Widget>[
//CalendarBar(),
Expanded(
child: ListView(
key: PageStorageKey('myListView'),
controller: listViewController,
children: <Widget>[
SizedBox(height: 30),
LiveSection(eventList: matches),
SizedBox(height: 20),
UpcomingSection(eventList: matches),
SizedBox(height: 20),
ResultsSection(eventList: matches)
],
),
)
],
)),
);
},
);
}
}
Apparently I fixed both problems by moving the ListView inside its own widget together with the Provider and ScrollController:
class _FixturesPageState extends State<FixturesPage> {
int selectedTap = 0;
Function setSelectedTap(int tap) {
setState(() {
selectedTap = tap;
});
return null;
}
Widget selectedSport = FavoritesTap();
Function setSelectedSport(Widget newSelectedSport) {
setState(() {
selectedSport = newSelectedSport;
});
return null;
}
#override
Widget build(BuildContext context) {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
return Navigator(
key: navigatorKey,
initialRoute: "/",
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(
builder: (context) => Container(
color: darkShade6,
child: Column(
children: <Widget>[
//CalendarBar(),
Expanded(child: FixturesList())
],
)),
);
},
);
}
}
class FixturesList extends StatelessWidget {
final ScrollController listViewController =
ScrollController(keepScrollOffset: true);
#override
Widget build(BuildContext context) {
List<dynamic> matches = Provider.of<List<SoccerMatch>>(context);
return ListView(
key: PageStorageKey('myListView'),
controller: listViewController,
children: <Widget>[
SizedBox(height: 30),
LiveSection(eventList: matches),
SizedBox(height: 20),
UpcomingSection(eventList: matches),
SizedBox(height: 20),
ResultsSection(eventList: matches)
],
);
}
}