Imagine I have a parent widget and a child widget. In the parent widget, there is a variable: 'page index'. And the child widget needs to change that variable in a set state method (because it has to be refreshed). How can I do this?
The body of the scaffold renders content based on the pageindex.
The code
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int pageIndex = 0;
List<Widget> pageList = <Widget>[
HomeScreen(),
ProfileScreen(),
CartScreen(),
ProfileScreen(),
ProfileScreen()
];
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CartViewModel()),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Home page',
theme: theme(),
onGenerateRoute: generateRoute,
home: Scaffold(
bottomNavigationBar: BottomNavBar(pageIndex, setPageIndex()),
body: pageList[pageIndex],
),
),
);
}
void setPageIndex(newValue) {
setState(() {
pageIndex = newValue;
});
}
}
class BottomNavBar extends StatefulWidget {
BottomNavBar(this.pageIndex, this.setPageIndexFunction);
int pageIndex;
VoidCallback setPageIndexFunction;
#override
State<BottomNavBar> createState() => _BottomNavBarState();
}
class _BottomNavBarState extends State<BottomNavBar> {
#override
Widget build(BuildContext context) {
CartViewModel cartViewModel = context.watch<CartViewModel>();
return BottomNavigationBar(
elevation: 5,
backgroundColor: Colors.white,
currentIndex: widget.pageIndex,
type: BottomNavigationBarType.fixed,
fixedColor: kAccentColor,
items: [
const BottomNavigationBarItem(
label: "Home", icon: Icon(Icons.home_outlined)),
const BottomNavigationBarItem(
label: "Search", icon: Icon(Icons.search_outlined)),
BottomNavigationBarItem(
label: "Cart",
icon: buildCustomBadge(
counter: cartViewModel.loading == false
? cartViewModel.cart!.count
: 0,
child: Icon(Icons.shopping_bag_outlined),
)),
const BottomNavigationBarItem(
label: "Favorite",
icon: Icon(Icons.favorite_border_outlined)),
const BottomNavigationBarItem(
label: "Profile", icon: Icon(Icons.person_outline))
],
onTap: (value) {
widget.setPageIndexFunction(value);
},
);
}
}
What is the problem
I am trying to call the function: "setPageIndexFunction(value)" and pass in the clicked value. Then it should call the function "setState" in the parent.
How can I do this?
The reason I put the bottomnavbar in a separate widget is because I could not write this line of code in the MyApp class: "CartViewModel cartViewModel = context.watch();". I need this line to check if an item has been added to my cart and show it in the bottomnavbar.
Or should I use provider for this?
Thanks!
Try these steps:
Use ValueChanged<int> instead of VoidCallback
class BottomNavBar extends StatefulWidget {
BottomNavBar(this.pageIndex, this.setPageIndexFunction);
int pageIndex;
ValueChanged<int> setPageIndexFunction;
#override
State<BottomNavBar> createState() => _BottomNavBarState();
}
Use setPageIndex in _MyAppState as a tear-off.
bottomNavigationBar: BottomNavBar(pageIndex, setPageIndex),
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 defined the whole CupertinoTabBar in class TabTwo in another file, but I cannot use TabTwo in main.dart.
error report:
lib/main.dart:24:15: Error: The argument type 'TabTwo' can't be assigned to the parameter type 'CupertinoTabBar'.
- 'TabTwo' is from 'package:untitled/tabtwo.dart' ('lib/tabtwo.dart').
- 'CupertinoTabBar' is from 'package:flutter/src/cupertino/bottom_tab_bar.dart' ('../../../../snap/flutter/common/flutter/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart').
tabBar: TabTwo(),
main.dart:
import 'package:flutter/cupertino.dart';
import 'tabtwo.dart';
void main() => runApp(const CupertinoTabBarApp());
class CupertinoTabBarApp extends StatelessWidget {
const CupertinoTabBarApp({super.key});
#override
Widget build(BuildContext context) {
return const CupertinoApp(
theme: CupertinoThemeData(brightness: Brightness.light),
home: CupertinoTabBarExample(),
);
}
}
class CupertinoTabBarExample extends StatelessWidget {
const CupertinoTabBarExample({super.key});
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: TabTwo(),
tabBuilder: (BuildContext context, int index) {
return const Text('example');
},
);
}
}
tabtwo.dart:
import 'package:flutter/cupertino.dart';
class TabTwo extends StatelessWidget {
const TabTwo({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CupertinoTabBar(
items: const [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.star_fill),
label: 'Favourites',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.clock_solid),
label: 'Recents',
),
],
);
}
}
I tried the code above but got an error.
How do I fix this?
The TabTwo needs to extend the CupertinoTabBar (not the StatelessWidget) in order to be used instead of CupertinoTabBar itself, like so:
class TabTwo extends CupertinoTabBar {
const TabTwo(List<BottomNavigationBarItem> items, {Key? key}) : super(items: items, key: key);
#override
Widget build(BuildContext context) {
return CupertinoTabBar(
items: const [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.star_fill),
label: 'Favourites',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.clock_solid),
label: 'Recents',
),
],
);
}
}
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,
),
),
);
}
}
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,
),
);
}
}
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
HomeScreen();
#override
_HomeScreenState createState() => new _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0;
final List<Widget> _children = [
MapsScreen(),
HistoryScreen(),
];
#override
void initState() {
super.initState();
RestAPI.loadMapsFromNetwork();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home screen'),
),
body: _children[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped, // new
currentIndex: _currentIndex,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.map),
title: new Text('Maps'),
),
BottomNavigationBarItem(
icon: new Icon(Icons.change_history),
title: new Text('History'),
)
],
),
);
}
void onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
}
This home.dart makes a network call in initState method.
How do I pass the list of maps that the client received from the network to one of the tabs like MapsScreen? Do I need to use ScopedModel or InheritedWidget or is there a better approach? All the logic to render is within the MapsScreen class.
You can pass the value from the json response like this.
class MapScreen extends StatefulWidget {
Map<List<String,dynamic>> data ;
MapScreen({this.data}) ;
_MapScreenState createState() => _MapScreenState() ;
}
class _MapScreenState extends State<MapScreen> {
#override
Widget build(BuildContext context) {
return Container(
child: ListView(
/* use the data over here */
),
);
}
}