Related
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();
}
}
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",
),
),
),
);
}
}
So, I have a "home" page, that has a scaffold, appbar, body, and bottomNavigationBar.
Body is loaded as widget, from a list, depending on what's clicked on bottomNavigationBar.
I'd like to be able to press something in the child widget currently loaded in parent body, and change the parent's body widget to another child widget, while passing a value to the child widget as well.
Think of it like pressing a link in an iframe in a browser, just the iframe changes and you can pass a value like ?id=12.
This is the "home" page:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _selectedIndex = 0;
final List<Widget> _children = [
Page1(),
Page2(),
Page3(),
Page4()
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
TextButton(
onPressed: () {},
child: Text("Log Out", style: TextStyle(color: Colors.white)),
),
],
backgroundColor: Colors.blue.shade900,
),
body: _children[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.work),
label: 'Page 1',
),
BottomNavigationBarItem(
icon: Icon(Icons.inventory),
label: 'Page 2',
),
BottomNavigationBarItem(
icon: Icon(Icons.alt_route),
label: 'Page 3',
),
BottomNavigationBarItem(
icon: Icon(Icons.article),
label: 'Page 4',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
type: BottomNavigationBarType.fixed,
),
);
}
}
And this is the child widget that would be defined by _children[_selectedIndex] by pressing one of the buttons in the bottomNavigationBar
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
#override
Widget build(BuildContext context) {
return Text("Click Here",onPressed:(){
//objective here is to open Page5 in the body of HomePage(not in _children list of homepage, and pass it some value (ie. id 12))
},
),
);
}
}
To exemplify expectation, this is what I'd like in Page5 (which should be loaded in the body of HomePage
class Page5 extends StatefulWidget {
#override
_Page5State createState() => _Page5State();
}
class _Page5State extends State<Page5> {
int _variableFromPage1;
#override
Widget build(BuildContext context) {
return Text(_variableFromPage1);
}
}
Can anyone advise the best way of doing that?
You can define custom callback function like the following code sample.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _selectedIndex = 0;
int _selectedID = 0;
List<Widget> _children;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
void initState() {
super.initState();
_children = [
Page1(
callback: (value) {
setState(() {
_selectedID = value;
});
},
),
Page2(),
];
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: (_selectedIndex == 0 && _selectedID > 0)
? Page5(id: _selectedID)
: _children[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.work),
label: 'Page 1',
),
BottomNavigationBarItem(
icon: Icon(Icons.inventory),
label: 'Page 2',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
type: BottomNavigationBarType.fixed,
),
);
}
}
typedef IntCallback(int value);
class Page1 extends StatelessWidget {
Page1({Key key, #required this.callback}) : super(key: key);
final IntCallback callback;
#override
Widget build(BuildContext context) {
return Center(
child: InkWell(
onTap: () {
callback(12);
},
child: Text("Page1 ::: id >>> 12"),
),
);
}
}
class Page2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text("Page2"),
);
}
}
class Page5 extends StatelessWidget {
Page5({Key key, #required this.id}) : super(key: key);
final int id;
#override
Widget build(BuildContext context) {
return Center(child: Text("Page5 ::: id $id"));
}
}
You try this way
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
// This widget is the root of your application.
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _selectedIndex = 0;
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case "Home":
return MaterialPageRoute(builder: (context) => Home());
case "Settings":
return MaterialPageRoute(builder: (context) => Settings());
default:
return MaterialPageRoute(builder: (context) => Default());
}
}
void _onItemTapped(int index) {
switch (index) {
case 1:
_navigatorKey.currentState!
.pushNamedAndRemoveUntil("Home", (route) => false);
break;
case 2:
_navigatorKey.currentState!
.pushNamedAndRemoveUntil("Settings", (route) => false);
break;
default:
_navigatorKey.currentState!
.pushNamedAndRemoveUntil("Default", (route) => false);
}
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Navigator(key: _navigatorKey, onGenerateRoute: generateRoute),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Setting',
),
],
showSelectedLabels: true,
showUnselectedLabels: false,
currentIndex: _selectedIndex,
selectedItemColor: Colors.white,
unselectedItemColor: Color(0xFF9e9e9e),
iconSize: 16,
backgroundColor: Color.fromRGBO(58, 66, 86, 1.0),
onTap: _onItemTapped,
),
),
);
}
}
basically I have a swiping screen with elements, where user is able to swipe in left or right direction. When the user is swiping, im calling some functions. Im using GestureDetector for gesture recognitions and PageView.Custom for my items. Probably ListView.Custom does also work, but it doesn't fix my issue I have.
I need a PageController, because I have to control the navigation programatically. And I think the PageController maybe is the reason behind my issue that my functions are called multiple times. How to fix it? Does somebody know why setstate is called that often and what to do to prevent it?
Im providing you a fully working example (minified version) with a print on the swiping right actions, where you can see that its beeing called multiple times.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int currentIndex = 0;
#override
Widget build(BuildContext context) {
// Page selector for tab list
void _selectPage(int index) {
print('page index: $index');
setState(() {
currentIndex = index;
});
}
// Routes list for tab navigation Android
final List<Widget> _pages = [
ScreenA(),
ScreenB(func: _selectPage),
];
return Scaffold(
appBar: AppBar(),
body: _pages[currentIndex],
bottomNavigationBar: SafeArea(
child: BottomNavigationBar(
onTap: _selectPage,
iconSize: 22,
currentIndex: currentIndex,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
backgroundColor: Theme.of(context).primaryColor,
icon: Icon(Icons.description),
label: 'ScreenA',
),
BottomNavigationBarItem(
backgroundColor: Theme.of(context).primaryColor,
icon: Icon(Icons.ac_unit_outlined),
label: 'ScreenB'),
],
),
),
);
}
}
class ScreenA extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Text('HOME'),
);
}
}
class ScreenB extends StatefulWidget {
Function func;
ScreenB({Key key, #required this.func})
: super(key: key);
#override
_ScreenBState createState() => _ScreenBState();
}
class _ScreenBState extends State<ScreenB> {
_ScreenBState();
var _controller = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.access_alarm_sharp),
onPressed: () async {
widget.func(0);
},
),
],
),
body: PageView.custom(
dragStartBehavior: DragStartBehavior.start,
controller: _controller,
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
childrenDelegate: SliverChildBuilderDelegate((ctx, pageIndex) =>
GestureDetector(
onPanUpdate: (details) async {
if (details.delta.dx < 0) {
_controller.nextPage(
duration: Duration(milliseconds: 200),
curve: Curves.easeInOut);
print('function called');
}
},
child: Center(
child: Container(
width: 200,
height: 200,
color: Colors.red,
child: Text('hi')))))),
);
}
}
Thanks in advance!
The problem is that you are using the onPanUpdate method, which is triggered every time a user drags their finger either right or left. You should use the onPanEnd method, which is only triggered when the user's finger is off the screen after dragging either left or right. The function below will work fine.
onPanEnd: (details) async { if (details.velocity.pixelsPerSecond.dx < 0) { _controller.nextPage( duration: Duration(milliseconds: 200), curve: Curves.easeInOut); print('function called'); } }
Please write this function out of widget build function
// Page selector for tab list
void _selectPage(int index) {
print('page index: $index');
setState(() {
currentIndex = index;
});
}
like this
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int currentIndex = 0;
// Page selector for tab list
void _selectPage(int index) {
print('page index: $index');
setState(() {
currentIndex = index;
});
}
// Routes list for tab navigation Android
final List<Widget> _pages = [
ScreenA(),
ScreenB(func: _selectPage),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: _pages[currentIndex],
bottomNavigationBar: SafeArea(
child: BottomNavigationBar(
onTap: _selectPage,
iconSize: 22,
currentIndex: currentIndex,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
backgroundColor: Theme.of(context).primaryColor,
icon: Icon(Icons.description),
label: 'ScreenA',
),
BottomNavigationBarItem(
backgroundColor: Theme.of(context).primaryColor,
icon: Icon(Icons.ac_unit_outlined),
label: 'ScreenB'),
],
),
),
);
}
}
class ScreenA extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Text('HOME'),
);
}
}
class ScreenB extends StatefulWidget {
Function func;
ScreenB({Key key, #required this.func})
: super(key: key);
#override
_ScreenBState createState() => _ScreenBState();
}
class _ScreenBState extends State<ScreenB> {
_ScreenBState();
var _controller = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.access_alarm_sharp),
onPressed: () async {
widget.func(0);
},
),
],
),
body: PageView.custom(
dragStartBehavior: DragStartBehavior.start,
controller: _controller,
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
childrenDelegate: SliverChildBuilderDelegate((ctx, pageIndex) =>
GestureDetector(
onPanUpdate: (details) async {
if (details.delta.dx < 0) {
_controller.nextPage(
duration: Duration(milliseconds: 200),
curve: Curves.easeInOut);
print('function called');
}
},
child: Center(
child: Container(
width: 200,
height: 200,
color: Colors.red,
child: Text('hi')))))),
);
}
}
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.