Flutter, how to programatically change index of BottomNavigationBar from another file? - flutter

I want to change my BottomNavigationBar's selected index from one of its items but that item is implemented in a different .dart file and is a separate StatefulWidget.
My BottomNavigationBar (navbar.dart):
class NavbarRouter extends StatefulWidget {
const NavbarRouter({Key? key}) : super(key: key);
#override
_NavbarRouterState createState() => _NavbarRouterState();
}
class _NavbarRouterState extends State<NavbarRouter> {
final List<Widget> pages = [
const YesillemePage(),
const YesillenecekPaletListesiPage(),
const PaletIcerigiPage(),
const RedPage()
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: selectedIndexGlobal,
children: pages,
),
bottomNavigationBar: SizedBox(
height: MediaQuery.of(context).size.height * .11,
child: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.bar_chart), label: "Yeşilleme\n Kontrol"),
BottomNavigationBarItem(
icon: Icon(Icons.featured_play_list_outlined),
label: "Yeşillenecek\n Palet"),
BottomNavigationBarItem(
icon: Icon(
Icons.content_paste_search,
color: Colors.grey,
),
label: "Palet İçeriği"),
BottomNavigationBarItem(
icon: Icon(Icons.warning_amber_outlined), label: "Red")
],
backgroundColor: Colors.orange[300],
currentIndex: selectedIndexGlobal,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.white,
selectedFontSize: 12.0,
unselectedFontSize: 10.0,
onTap: (index) {
setState(() {
if (index == 0) {
focusNodeY1!.requestFocus();
} else if (index == 3) {
focusNodeRed!.requestFocus();
} else if (index == 2) {
return;
}
selectedIndexGlobal = index;
});
},
),
));
}
}
And the place I want it to change (greenitem.dart, 2nd item):
DataRow(onLongPress: () {
//here i wanto to go to index 2
},
color: item.ACIL == "X"
? MaterialStateProperty.all<
Color>(Colors.red)
: MaterialStateProperty.all<
Color>(Colors.white),
cells: [
DataCell(Text(item.PLTNO!)),
DataCell(Text(
item.BOLUM!.toString())),
]))
What I tried:
onLongPress: () {
setState(){selectedIndexGlobal = 2;}
},
This does not refresh the state of the navbar so didn't work.
And I tried to give a GlobalKey to my NavbarRouter and
onLongPress: () {
navbarKey.currentState!.setState(() {selectedIndexGlobal = 2});
},
But that gave me a "duplicate global key detected in widget tree" error. What should I do?

Please refer to below example code
ValueListenableBuilder widget. It is an amazing widget. It builds the widget every time the valueListenable value changes. Its values remain synced with there listeners i.e. whenever the values change the ValueListenable listen to it. It updates the UI without using setState() or any other state management technique.
In Dart, a ValueNotifier is a special type of class that extends a ChangeNotifer . ... It can be an int , a String , a bool or your own data type. Using a ValueNotifier improves the performance of Flutter app as it can help to reduce the number times a widget gets rebuilt.
ValueListenableBuilder will listen for changes to a value notifier and automatically rebuild its children when the value changes.
ValueNotifer & ValueListenableBuilder can be used to hold value and update widget by notifying its listeners and reducing number of times widget tree getting rebuilt.
For example refer to this link
void main() {
runApp(MyApp());
}
final ValueNotifier selectedIndexGlobal = ValueNotifier(0); // Add this ValueNotifier which is globally accessible throughtout your project
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: NavbarRouter(),
);
}
}
class YesillemePage extends StatelessWidget {
const YesillemePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.pink,
);
}
}
class YesillenecekPaletListesiPage extends StatelessWidget {
const YesillenecekPaletListesiPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.purple,
);
}
}
class PaletIcerigiPage extends StatelessWidget {
const PaletIcerigiPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue,
);
}
}
class NavbarRouter extends StatefulWidget {
const NavbarRouter({Key key}) : super(key: key);
#override
_NavbarRouterState createState() => _NavbarRouterState();
}
class _NavbarRouterState extends State<NavbarRouter> {
final List<Widget> pages = [
YesillemePage(),
YesillenecekPaletListesiPage(),
PaletIcerigiPage(),
RedPage(),
];
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: selectedIndexGlobal,
builder: (context, val, child) {
return Scaffold(
body: IndexedStack(
index: selectedIndexGlobal.value,
children: pages,
),
bottomNavigationBar: SizedBox(
height: MediaQuery.of(context).size.height * .11,
child: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.bar_chart), label: "Yeşilleme\n Kontrol"),
BottomNavigationBarItem(
icon: Icon(Icons.featured_play_list_outlined),
label: "Yeşillenecek\n Palet"),
BottomNavigationBarItem(
icon: Icon(
Icons.search,
color: Colors.grey,
),
label: "Palet İçeriği"),
BottomNavigationBarItem(
icon: Icon(Icons.warning_amber_outlined), label: "Red")
],
backgroundColor: Colors.orange[300],
currentIndex: selectedIndexGlobal.value,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.white,
selectedFontSize: 12.0,
unselectedFontSize: 10.0,
onTap: (index) {
if (index == 0) {
focusNodeY1!.requestFocus();
} else if (index == 3) {
focusNodeRed!.requestFocus();
} else if (index == 2) {
return;
}
selectedIndexGlobal.value = index;
},
),
),
);
},
);
}
}
class RedPage extends StatefulWidget {
const RedPage({Key key}) : super(key: key);
#override
State<RedPage> createState() => _RedPageState();
}
class _RedPageState extends State<RedPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: () {
selectedIndexGlobal.value = 0;
},
child: Text(
"Change Index",
),
),
),
);
}
}

Related

Why doesn't BottomNavigatorBarItem currentIndex get updated?

I am new to flutter and I did find similar questions on SO but am too novice to understand the nuance so apologies if this question is too similar to an already asked one.
I have a BottomNavigationBar which has 3 icons (Home, Play and Create) which should navigate between these 3 routes/pages.
main.dart
routes: {
"/home": (context) => MyHomePage(title: "STFU"),
"/play": (context) => Play(),
"/create": (context) => Create(),
"/settings": (context) => Settings(),
},
I extracted my navbar into a custom class so my 3 separate pages could use it:
bottom-nav.dart
class MyBottomNavBar extends StatefulWidget {
MyBottomNavBar({
Key? key,
}) : super(key: key);
#override
State<MyBottomNavBar> createState() => _MyBottomNavBarState();
}
class _MyBottomNavBarState extends State<MyBottomNavBar> {
int _selectedIndex = 0;
void _onTapped(int index) => {
print("_onTapped called with index = $index"),
setState(
() => _selectedIndex = index,
)
};
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
backgroundColor: Colors.orangeAccent[100],
currentIndex: _selectedIndex,
onTap: (value) => {
print("value is $value"),
// find index and push that
_onTapped(value),
if (value == 0)
{Navigator.pushNamed(context, "/home")}
else if (value == 1)
{Navigator.pushNamed(context, "/play")}
else if (value == 2)
{Navigator.pushNamed(context, "/create")}
},
items: [
BottomNavigationBarItem(label: "Home", icon: Icon(Icons.home_filled)),
BottomNavigationBarItem(
label: "Play", icon: Icon(Icons.play_arrow_rounded)),
BottomNavigationBarItem(label: "Create", icon: Icon(Icons.create)),
],
);
}
}
so now i just set this MyBottomNavBar class to the bottomNavigationBar property of the Scaffold widget my inside Home page, Play page and Create page for eg.
home.dart
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home"),
),
body: Column(
children: [
Container(
padding: EdgeInsets.all(20.00),
child: Text("inside home"),
),
],
),
bottomNavigationBar: MyBottomNavBar(),
);
}
}
play.dart
class Play extends StatefulWidget {
const Play({super.key});
#override
State<Play> createState() => _PlayState();
}
class _PlayState extends State<Play> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("inside play page"),
SizedBox(
height: 30.00,
),
Text("some text"),
],
),
),
bottomNavigationBar: MyBottomNavBar(),
);
}
}
The nav bar buttons work to switch between pages but for some reason the currentIndex value isn't getting updated and stays at 0 (i.e on the "Home" icon). When I debug it I can see _selectedIndex getting updated inside inside the _onTapped function which should update the currentIndex value but it doesn't appear to do so. Any help would be appreciated
What you have here is three different pages with their separate BottomNavBar class instance. Instead you should have a shared Scaffold and one bottomNavBar so that when you navigate bottomNavbar state does not reset.
You can use PageView to do this.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
super.initState();
}
List<Widget> pages = const [Home(), Play(), Create()];
final _pageController = PageController();
int _selectedIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
_pageController.jumpToPage(index);
_selectedIndex = index;
setState(() {});
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.play_arrow), label: 'Play'),
BottomNavigationBarItem(icon: Icon(Icons.create), label: 'Create'),
],
backgroundColor: Colors.greenAccent,
),
body: PageView(
controller: _pageController,
children: pages,
),
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}
class Play extends StatelessWidget {
const Play({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}
class Create extends StatelessWidget {
const Create({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}

My screen doesn't reflect the changes in my app though the setState method works well

I'm trying to call a StatefulWidget(i.e FirstPage()) within a MaterialApp. I'm pretty much new to flutter and I don't know where I went wrong. According to my knownledge I've used StatefulWidget to tell flutter my screen on that page is going to encounter some changes in UI. But I got no idea to fix this error.
main.dart file:
import 'package:flutter/material.dart';
import 'package:flutter_project/main.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage());
}
}
class FirstPage extends StatefulWidget {
const FirstPage({Key? key}) : super(key: key);
#override
State<FirstPage> createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
#override
Widget build(BuildContext context) {
String buttonName = "Click";
int currentIndex = 0;
return Scaffold(
appBar: AppBar(
title: const Text("App title "),
),
body: Center(
child: currentIndex == 0
? Container(
width: double.infinity,
height: double.infinity,
color: Colors.blueAccent,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 280,
height: 80,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: BorderRadius.circular(18),
),
backgroundColor: const Color.fromRGBO(9, 8, 99, 90),
foregroundColor: Colors.red,
),
onPressed: () {
setState(() {
buttonName = "Clicked";
//print(buttonName0);
});
},
child: Text(buttonName),
),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
//'BuildContext' - datatype and 'context' - variable name
return const SecondPage();
},
),
);
},
child: const Text("Move to new page"),
),
],
),
)
: Image.asset("images/img.png"),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(label: "Home", icon: Icon(Icons.home)),
BottomNavigationBarItem(label: "Settings", icon: Icon(Icons.settings))
],
currentIndex: currentIndex,
onTap: (int index) {
//index value here is returned by flutter by the function 'onTap'
setState(() {
currentIndex = index;
//print(currentIndex);
});
},
),
); //Scaffold represents the skeleton of the app(displays white page)
}
}
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
);
}
}
Images:
Before pressing Click and Settings button
After pressing Click and Settings looks the same
I want the screen to change the ElevatedButton Click to Clicked when onPressed() is triggered and also, the app should be able to switch settings page when the onTap() method is triggered in the bottom navigation bar.
The code worked initially when I refrained from calling an entire page of Scaffold from Material app, but as soon as I changed the part
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage()); //<-- this part
}
}
I'm getting this error.
Put your variables outside the build method.Else it will reset to default on every build.
It will be like
class _FirstPageState extends State<FirstPage> {
//here
String buttonName = "Click";
int currentIndex = 0;
#override
Widget build(BuildContext context) {
// Not here
return Scaffold(
appBar: AppBar(
More about StatefulWidget

How to change the BottomNavigationBar after a Navigator.push?

I would like to set a new BottomNavigationBar after i've clicked on one of my ListTile. Right now, i am getting two BottomNavigationbar after I've clicked on one of them.
Below is my code where I setup the first bar:
class CoachNav extends StatefulWidget {
const CoachNav({Key? key}) : super(key: key);
#override
State<CoachNav> createState() => _CoachNavState();
}
class _CoachNavState extends State<CoachNav> {
int _selectedIndex = 0;
final List<Widget> _widgetOptions = <Widget>[
const TeamListView(),
const SettingsFormView(),
];
Widget? _onItemTap(int index) {
setState(() {
_selectedIndex = index;
});
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.groups),
label: "Mes équipes",
),
BottomNavigationBarItem(
icon: Icon(Icons.settings), label: "Paramètres"),
],
currentIndex: _selectedIndex,
onTap: _onItemTap,
),
);
}
}
Then, there is the code where I setup the second bar:
class TeamNav extends StatefulWidget {
const TeamNav({Key? key}) : super(key: key);
#override
State<TeamNav> createState() => _TeamNavState();
}
class _TeamNavState extends State<TeamNav> {
int _selectedIndex = 0;
final List<Widget> _widgetOptions = <Widget>[
const PlayersListView(),
const GamesListView(),
];
Widget? _onItemTap(int index) {
setState(() {
_selectedIndex = index;
});
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.group),
label: "Mes joueurs",
),
BottomNavigationBarItem(
icon: Icon(Icons.sports_basketball), label: "Mes matchs"),
],
currentIndex: _selectedIndex,
onTap: _onItemTap,
),
);
}
}
Here are two screenshots of what is happening
First Bar
Second Bar
---------------- EDIT ----------------------
This is what I get when I make the _widgetOptions texts
I got the first bar... whith the content from where the second bar should Appear
this is the snippet of the code I got as answer below:
lass App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(home: CoachNav());
}
}
class TeamNav extends StatefulWidget {
const TeamNav({Key? key}) : super(key: key);
#override
State<TeamNav> createState() => _TeamNavState();
}
class _TeamNavState extends State<TeamNav> {
int _selectedIndex = 0;
final List<Widget> _widgetOptions = <Widget>[
Text("PlayersListView"),
Text("PlayersListView"),
];
Widget? _onItemTap(int index) {
setState(() {
_selectedIndex = index;
});
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
/* floatingActionButton: FloatingActionButton(onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CoachNav(),
));
}),*/
body: Container(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.group),
label: "Mes joueurs",
),
BottomNavigationBarItem(
icon: Icon(Icons.sports_basketball), label: "Mes matchs"),
],
currentIndex: _selectedIndex,
onTap: _onItemTap,
),
);
}
}
class CoachNav extends StatefulWidget {
const CoachNav({Key? key}) : super(key: key);
#override
State<CoachNav> createState() => _CoachNavState();
}
class _CoachNavState extends State<CoachNav> {
int _selectedIndex = 0;
final List<Widget> _widgetOptions = <Widget>[
Text("PlayersListView"),
Text("PlayersListView"),
];
Widget? _onItemTap(int index) {
setState(() {
_selectedIndex = index;
});
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: _widgetOptions.elementAt(_selectedIndex),
),
//floatingActionButton: FloatingActionButton(onPressed: () {}),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.groups),
label: "Mes équipes",
),
BottomNavigationBarItem(
icon: Icon(Icons.settings), label: "Paramètres"),
],
currentIndex: _selectedIndex,
onTap: _onItemTap,
),
);
}
}
This is the code that have the text shown in the third screenshot.
class PlayersListView extends StatelessWidget {
const PlayersListView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: Text("Playerslist"),);
}
}
I think you have a design problem.
In your case, the best way is this one, i think.
When you tap on a tile you should fix a flag and rebuild your page instead of navigating to a new route.
Then, when building your BottomNavigationBarItemlist check the flag and add or remove BottomNavigationBarItem as you need.
Remove extra scaffold from _widgetOptions children that contains bottomNavBar. Follow the snippet pattern
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(home: TeamNav());
}
}
class TeamNav extends StatefulWidget {
const TeamNav({Key? key}) : super(key: key);
#override
State<TeamNav> createState() => _TeamNavState();
}
class _TeamNavState extends State<TeamNav> {
int _selectedIndex = 0;
final List<Widget> _widgetOptions = <Widget>[
Text("PlayersListView"),
Text(" GamesListView()"),
];
Widget? _onItemTap(int index) {
setState(() {
_selectedIndex = index;
});
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CoachNav(),
));
}),
body: Container(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.group),
label: "Mes joueurs",
),
BottomNavigationBarItem(
icon: Icon(Icons.sports_basketball), label: "Mes matchs"),
],
currentIndex: _selectedIndex,
onTap: _onItemTap,
),
);
}
}
class CoachNav extends StatefulWidget {
const CoachNav({Key? key}) : super(key: key);
#override
State<CoachNav> createState() => _CoachNavState();
}
class _CoachNavState extends State<CoachNav> {
int _selectedIndex = 0;
final List<Widget> _widgetOptions = <Widget>[
Text("TeamListView"),
Text("SettingsFormView"),
];
Widget? _onItemTap(int index) {
setState(() {
_selectedIndex = index;
});
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: _widgetOptions.elementAt(_selectedIndex),
),
floatingActionButton: FloatingActionButton(onPressed: () {}),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.groups),
label: "Mes équipes",
),
BottomNavigationBarItem(
icon: Icon(Icons.settings), label: "Paramètres"),
],
currentIndex: _selectedIndex,
onTap: _onItemTap,
),
);
}
}
Ok so I found a solution, I simply needed to add "rootNavigator: true" to my Navigator.push. It works as intended now.

Callback from child to parent Flutter

I am new to flutter. I am trying to separate the bottomappbar widget from my home screen. Thing is, it is I need to send back the index to the home screen file so I can switch the body of the screen. I've been learning BloC lately, but I think it is an overkill for this case, even though I am going to use it in other parts of the app (hopefully this is the right assumption). So, how can I send the index to the parent?
Parent
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final _bottomNavigationPages = [
Screen1(),
Screen2(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(
color: Colors.blueGrey,
),
title:
Text('xxx', style: new TextStyle(fontWeight: FontWeight.w400)),
),
body: _bottomNavigationPages[_selectedIndex],
bottomNavigationBar: HomeBottomAppBar(),
);
}
}
Child
class HomeBottomAppBar extends StatefulWidget {
#override
_HomeBottomAppBarState createState() => _HomeBottomAppBarState();
}
class _HomeBottomAppBarState extends State<HomeBottomAppBar> {
int _selectedIndex = 0;
void _itemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
return BottomAppBar(
shape: CircularNotchedRectangle(),
notchMargin: 5.0,
clipBehavior: Clip.antiAlias,
child: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.x), title: Text("1")),
BottomNavigationBarItem(
icon: Icon(Icons.x), title: Text("2")),
],
currentIndex: _selectedIndex,
onTap: _itemTapped,
),
);
}
}
Also, I am going under the assumption that this is good practice. Maybe it is just better to have everything in the same file.
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final _bottomNavigationPages = [
Screen1(),
Screen2(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(color: Colors.blueGrey),
title: Text('xxx', style: new TextStyle(fontWeight: FontWeight.w400)),
),
body: _bottomNavigationPages[_selectedIndex],
bottomNavigationBar: HomeBottomAppBar(refresh: _refresh),
);
}
void _refresh(int index) {
setState(() {
_selectedIndex = index;
});
}
}
class HomeBottomAppBar extends StatefulWidget {
final Function refresh;
const HomeBottomAppBar({Key key, this.refresh}) : super(key: key);
#override
_HomeBottomAppBarState createState() => _HomeBottomAppBarState();
}
class _HomeBottomAppBarState extends State<HomeBottomAppBar> {
int _selectedIndex = 0;
void _itemTapped(int index) {
_selectedIndex = index;
widget.refresh(index);
}
#override
Widget build(BuildContext context) {
return BottomAppBar(
shape: CircularNotchedRectangle(),
notchMargin: 5.0,
clipBehavior: Clip.antiAlias,
child: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.x), title: Text("1")),
BottomNavigationBarItem(icon: Icon(Icons.x), title: Text("2")),
],
currentIndex: _selectedIndex,
onTap: _itemTapped,
),
);
}
}

Flutter persistent app bar across PageView

Ideally I would like to set up my Flutter app as follows
PageView to swipe left/right between 3 pages and a bottom navigation bar to serve as a label and also help with navigation
Persistent appbar on top with drawer and contextual icons
Page content in between
As can be seen in the image, I have this mostly set up the way I would like in the following manner
main.dart - app entry point, set up appbar, set up pageview with children for new PeoplePage, new TimelinePage, new StatsPage
people_page.dart
timeline_page.dart
stats_page.dart
These three pages just deliver the content to the PageView children as required.
Is this the correct way to achieve this? On the surface it works fine. The issue I am coming across is that on the people page I want to implement a selectable list that changes the appbar title/color as in this example, but the appbar is set up on the main page. Can I access the appbar globally?
I could build a new appbar for each page, but I dont want a new appbar swiping in when switching page. I'd prefer the appbar to look persistent and only have the content swipe in.
Any guidance on the best way to accomplish this would be appreciated.
I put together a quick example of how you might communicate from your screen down to the pages and then also back again. This should solve your problem.
https://gist.github.com/slightfoot/464fc225b9041c2d66ec8ab36fbdb935
import 'package:flutter/material.dart';
void main() => runApp(TestApp());
class TestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.green[900],
scaffoldBackgroundColor: Colors.grey[200],
),
home: MainScreen(),
);
}
}
class AppBarParams {
final Widget title;
final List<Widget> actions;
final Color backgroundColor;
AppBarParams({
this.title,
this.actions,
this.backgroundColor,
});
}
class MainScreen extends StatefulWidget {
final int initialPage;
const MainScreen({
Key key,
this.initialPage = 0,
}) : super(key: key);
#override
MainScreenState createState() => MainScreenState();
static MainScreenState of(BuildContext context) {
return context.ancestorStateOfType(TypeMatcher<MainScreenState>());
}
}
class MainScreenState extends State<MainScreen> {
final List<GlobalKey<MainPageStateMixin>> _pageKeys = [
GlobalKey(),
GlobalKey(),
GlobalKey(),
];
PageController _pageController;
AppBarParams _params;
int _page;
set params(AppBarParams value) {
setState(() => _params = value);
}
#override
void initState() {
super.initState();
_page = widget.initialPage ?? 0;
_pageController = PageController(initialPage: _page);
WidgetsBinding.instance.addPostFrameCallback((_) {
_pageKeys[0].currentState.onPageVisible();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: _params?.title,
actions: _params?.actions,
backgroundColor: _params?.backgroundColor,
),
body: PageView(
controller: _pageController,
onPageChanged: _onPageChanged,
children: <Widget>[
PeoplePage(key: _pageKeys[0]),
TimelinePage(key: _pageKeys[1]),
StatsPage(key: _pageKeys[2]),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _page,
onTap: _onBottomNavItemPressed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
title: Text('people'),
icon: Icon(Icons.people),
),
BottomNavigationBarItem(
title: Text('timeline'),
icon: Icon(Icons.history),
),
BottomNavigationBarItem(
title: Text('stats'),
icon: Icon(Icons.pie_chart),
),
],
),
);
}
#override
void reassemble() {
super.reassemble();
_onPageChanged(_page);
}
void _onPageChanged(int page) {
setState(() => _page = page);
_pageKeys[_page].currentState.onPageVisible();
}
void _onBottomNavItemPressed(int index) {
setState(() => _page = index);
_pageController.animateToPage(
index,
duration: Duration(milliseconds: 400),
curve: Curves.fastOutSlowIn,
);
}
}
abstract class MainPageStateMixin<T extends StatefulWidget> extends State<T> {
void onPageVisible();
}
class PeoplePage extends StatefulWidget {
const PeoplePage({Key key}) : super(key: key);
#override
PeoplePageState createState() => PeoplePageState();
}
class PeoplePageState extends State<PeoplePage> with MainPageStateMixin {
final List<Color> _colors = [
Colors.orange,
Colors.purple,
Colors.green,
];
int _personCount = 3;
#override
void onPageVisible() {
MainScreen.of(context).params = AppBarParams(
title: Text('People'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.person_add),
onPressed: () => setState(() => _personCount++),
),
],
backgroundColor: Colors.green,
);
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _personCount,
itemBuilder: (BuildContext context, int index) {
return Card(
child: InkWell(
onTap: () => _onTapCard(index),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Material(
type: MaterialType.circle,
color: _colors[index % _colors.length],
child: Container(
width: 48.0,
height: 48.0,
alignment: Alignment.center,
child: Text('$index', style: TextStyle(color: Colors.white)),
),
),
SizedBox(width: 16.0),
Text(
'Item #$index',
style: TextStyle(
color: Colors.grey[600],
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
},
);
}
void _onTapCard(int index) {
Scaffold.of(context).showSnackBar(SnackBar(content: Text('Item #$index')));
}
}
class TimelinePage extends StatefulWidget {
const TimelinePage({Key key}) : super(key: key);
#override
TimelinePageState createState() => TimelinePageState();
}
class TimelinePageState extends State<TimelinePage> with MainPageStateMixin {
#override
void onPageVisible() {
MainScreen.of(context).params = AppBarParams(
title: Text('Timeline'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.alarm_add),
onPressed: () {},
),
],
backgroundColor: Colors.purple,
);
}
#override
Widget build(BuildContext context) {
return Center(
child: Text('Coming soon'),
);
}
}
class StatsPage extends StatefulWidget {
const StatsPage({Key key}) : super(key: key);
#override
StatsPageState createState() => StatsPageState();
}
class StatsPageState extends State<StatsPage> with MainPageStateMixin {
#override
void onPageVisible() {
MainScreen.of(context).params = AppBarParams(
title: Text('Stats'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add_box),
onPressed: () {},
),
],
backgroundColor: Colors.orange,
);
}
#override
Widget build(BuildContext context) {
return Center(
child: Text('Coming soon'),
);
}
}
One way to tackle this would be to have the AppBar title and background color as state variables, and in your PageView set the onPageChanged to a function. This function takes in the page int and based on the page int it sets the state of the title and color to the values that you desire. For the multiselect list you set the title to the variable which keeps the values you have selected, may be keep it as a state variable in the main page and pass it down to the child component. You can use any of the state management strategies and that should probably work fine.
Example of onPageChanged function:
void onPageChanged(int page) {
String _temptitle = "";
Color _tempColor;
switch (page) {
case 0:
_temptitle = "People";
_tempColor = Colors.pink;
break;
case 1:
_temptitle = "Timeline";
_tempColor = Colors.green;
break;
case 2:
_temptitle = "Stats";
_tempColor = Colors.deepPurple;
break;
}
setState(() {
this._page = page;
this._title = _temptitle;
this._appBarColor = _tempColor;
});
}
So for the multiselect case, instead of setting the title to some constant you set the title to the variable which holds the values of the selected options.
Full code is here:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
PageController _pageController;
int _page = 0;
String _title = "MyApp";
Color _appBarColor = Colors.pink;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(_title),
backgroundColor: _appBarColor,
),
body: PageView(
children: <Widget>[
Container(
child: Center(child: Text("People")),
),
Container(
child: Center(child: Text("Timeline")),
),
Container(
child: Center(child: Text("Stats")),
),
],
controller: _pageController,
onPageChanged: onPageChanged,
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.people),
title: Text("People"),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_time),
title: Text("Timeline"),
),
BottomNavigationBarItem(
icon: Icon(Icons.pie_chart),
title: Text("Stats"),
),
],
onTap: navigateToPage,
currentIndex: _page,
),
);
}
void navigateToPage(int page) {
_pageController.animateToPage(page,
duration: Duration(milliseconds: 300), curve: Curves.ease);
}
void onPageChanged(int page) {
String _temptitle = "";
Color _tempColor;
switch (page) {
case 0:
_temptitle = "People";
_tempColor = Colors.pink;
break;
case 1:
_temptitle = "Timeline";
_tempColor = Colors.green;
break;
case 2:
_temptitle = "Stats";
_tempColor = Colors.deepPurple;
break;
}
setState(() {
this._page = page;
this._title = _temptitle;
this._appBarColor = _tempColor;
});
}
#override
void initState() {
super.initState();
_pageController = new PageController();
_title = "People";
}
#override
void dispose() {
super.dispose();
_pageController.dispose();
}
}
You can improve this code for your needs. Hope this was helpful in someway. Let me know if there is something I can improve about this answer.