How to prevent GoogleMap widget from rebuilding when changing its parent? - flutter

The concept is simple. I created a Scaffold with bottomNavigationbar that can replace the body of Scaffold when tapped. OrderPage should contain a GoogleMap widget.
class MainScreen extends StatefulWidget {
final String routeName = '/home';
final List<Widget> list = [
OrderPage(),
PaymentPage(),
AccountPage(),
];
#override
MainScreenState createState() => MainScreenState();
}
class MainScreenState extends State<MainScreen> {
int index = 0;
MainScreenState();
void navigationHandler(int value) {
setState(() {
index = value;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.airport_shuttle), title: Text("Book")),
BottomNavigationBarItem(
icon: Icon(Icons.attach_money), title: Text("Payment")),
BottomNavigationBarItem(
icon: Icon(Icons.account_box), title: Text("Account")),
],
selectedItemColor: Colors.red,
currentIndex: index,
onTap: navigationHandler,
),
body: widget.list[index],
),
);
}
}
The OrderPage:
class OrderPage extends StatefulWidget {
OrderPage({Key key}) : super(key: key);
#override
OrderPageState createState() {
// TODO: implement createState
return OrderPageState();
}
}
class OrderPageState extends State<OrderPage> {
GoogleMapController mapController;
final LatLng _center = const LatLng(45.521563, -2.677433);
void _onMapCreated(GoogleMapController controller) {
mapController = controller;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("map sample app"),
backgroundColor: Colors.green[700],
),
body: GoogleMap(onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(target: _center, zoom: 11.0));,
);
}
}
The current problem of this implementation is every time i changed tabs and went back to the OrderPage , it seems to automatically rebuild, which cost a request.
I tried using PageStorage to save the state of OrderPage, but it is still rebuilt.
Any idea or concept or suggestion on preventing OrderPage to rebuild is welcomed.

I managed to prevent the map from reloading by using IndexedStack for the body of my Scaffold. This way all pages can be stored and will not be rebuilt when i switch tabs.

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();
}
}

When I am adding another item in the bottom bar, the icon and the text turns white

I am working with the bottom bar from flutter templates called persistent bottombarbut when I started adding items in the bottom bar, the icons and text turns white.
As you can see here I added a new tab4 compared to the original code. if you try to bring it back to only three tabs the color will go back.
class HomePage extends StatelessWidget {
final _tab1navigatorKey = GlobalKey<NavigatorState>();
final _tab2navigatorKey = GlobalKey<NavigatorState>();
final _tab3navigatorKey = GlobalKey<NavigatorState>();
final _tab4navigatorKey = GlobalKey<NavigatorState>();
HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return PersistentBottomBarScaffold(
items: [
PersistentTabItem(
tab: const TabPage1(),
icon: Icons.home,
title: 'Home',
navigatorkey: _tab1navigatorKey,
),
PersistentTabItem(
tab: const TabPage2(),
icon: Icons.search,
title: 'Search',
navigatorkey: _tab2navigatorKey,
),
PersistentTabItem(
tab: const TabPage3(),
icon: Icons.person,
title: 'Profile',
navigatorkey: _tab3navigatorKey,
),
PersistentTabItem(
tab: const TabPage4(),
icon: Icons.person,
title: 'Profile',
navigatorkey: _tab4navigatorKey,
),
],
);
}
}
class TabPage1 extends StatelessWidget {
const TabPage1({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Tab 1')),
);
}
}
class TabPage2 extends StatelessWidget {
const TabPage2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold();
}
}
class TabPage3 extends StatelessWidget {
const TabPage3({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold();
}
}
class TabPage4 extends StatelessWidget {
const TabPage4({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold();
}
}
class PersistentBottomBarScaffold extends StatefulWidget {
/// pass the required items for the tabs and BottomNavigationBar
final List<PersistentTabItem> items;
const PersistentBottomBarScaffold({Key? key, required this.items})
: super(key: key);
#override
_PersistentBottomBarScaffoldState createState() =>
_PersistentBottomBarScaffoldState();
}
class _PersistentBottomBarScaffoldState
extends State<PersistentBottomBarScaffold> {
int _selectedTab = 0;
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
/// Check if curent tab can be popped
if (widget.items[_selectedTab].navigatorkey?.currentState?.canPop() ??
false) {
widget.items[_selectedTab].navigatorkey?.currentState?.pop();
return false;
} else {
// if current tab can't be popped then use the root navigator
return true;
}
},
child: Scaffold(
/// Using indexedStack to maintain the order of the tabs and the state of the
/// previously opened tab
body: IndexedStack(
index: _selectedTab,
children: widget.items
.map((page) => Navigator(
/// Each tab is wrapped in a Navigator so that naigation in
/// one tab can be independent of the other tabs
key: page.navigatorkey,
onGenerateInitialRoutes: (navigator, initialRoute) {
return [
MaterialPageRoute(builder: (context) => page.tab)
];
},
))
.toList(),
),
/// Define the persistent bottom bar
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedTab,
onTap: (index) {
setState(() {
_selectedTab = index;
});
},
items: widget.items
.map((item) => BottomNavigationBarItem(
icon: Icon(item.icon), label: item.title))
.toList(),
),
),
);
}
}
/// Model class that holds the tab info for the [PersistentBottomBarScaffold]
class PersistentTabItem {
final Widget tab;
final GlobalKey<NavigatorState>? navigatorkey;
final String title;
final IconData icon;
PersistentTabItem(
{required this.tab,
this.navigatorkey,
required this.title,
required this.icon});
}
Can you provide a snippet of your code? Looking at the persistent bottombar sample code, my first assumption is that you may need to assign colors to the selectedItemColor and unselectedItemColor parameters:
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
selectedItemColor: const Color(0xff6200ee),
unselectedItemColor: const Color(0xff757575),
type: _bottomNavType,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
items: _navBarItems),
);
This is all you need, this fixed the problem!
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: // ...,
)

When state of widget is created

I have two tabA, tabB and these are switched by user tapping.
What I want to do is
At first tabA opens.
tabB text changes depending on tabA state.
user opens tabB,there comes changed text.
My basic idea is getting the tabB state via object key.
However there is a problem,
Before opening tabB, state of tabB is not created.
These are my source code tabA is playerPage, tabB is settingPage
class _MainFrameState extends State<MainFrame>{
void _onItemTapped(int index) => setState(() => _selectedIndex = index );
int _selectedIndex = 0;
Widget settingPage = Text("");
Widget playerPage = Text("");
GlobalKey<_PlayerPageState> _playerPageKey = new GlobalKey<_PlayerPageState>();
GlobalKey<_SettingPageState> _settingPageKey = new GlobalKey<_SettingPageState>();
#override
void initState() {
print("Main Frame init state");
super.initState();
playerPage = PlayerPage(key:_playerPageKey);
settingPage = SettingPage(key:_settingPageKey);
}
function whenSomethingChanged(){ //this is called by push button or some user action,so initState() is already called.
print(playerPage.currentState!) // it has state and operatable.
print(settingPage.currentState!) // I want to change the tabB text but it returns null.
}
#override
Widget build(BuildContext context) {
print("Main Frame build");
List<Widget> _pages = [
playerPage,
settingPage
];
return Scaffold(
appBar: EmptyAppBar(),
body:Container(
decoration: new BoxDecoration(color: Colors.red),
child:Center(child:_pages.elementAt(_selectedIndex),)),
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Color(0xffC03410),
unselectedItemColor: Color(0xffE96D2F),
backgroundColor: Color(0xffF7BF51),
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.play_circle),
label: 'Player',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Setting',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
);
}
}
I can tell that when first build is called PlayerPage is created but SettingPage is not.
However I want to control the SettinPage , before it show.
What is the solution for this problem??
Since you didn't tell what kind of state of TabA you want to show on TabB, I will assume it's a String and wrote this. There's a lot of ways of doing what you want to achieve, this is just one of them.
Step 1: Pass the value of PlayerPage state to SettingPage like this:
class SettingPage extends StatefulWidget {
const SettingPage({
Key? key,
required this.text,
}) : super(key: key);
final String text;
#override
_SettingPageState createState() => _SettingPageState();
}
class _SettingPageState extends State<SettingPage> {
#override
Widget build(BuildContext context) {
return Center(child: Text(widget.text));
}
}
Step 2: Let's assume state you want to show on SettingPage is from TextField. Maybe a player name. Then you'll need to pass that textField value to MainFrame() widget whenever it changes.
class PlayerPage extends StatefulWidget {
const PlayerPage({
Key? key,
required this.onTextChanged,
}) : super(key: key);
final Function(String) onTextChanged;
#override
_PlayerPagetate createState() => _PlayerPagetate();
}
class _PlayerPagetate extends State<PlayerPage> {
final TextEditingController _controller = TextEditingController();
#override
Widget build(BuildContext context) {
return Center(
child: TextField(
controller: _controller,
onChanged: (value) {
widget.onTextChanged(value);
},
),
);
}
}
Step 3: On MainFrame widget, now you get playerName into _playerName variable from PlayerPlage and pass it into SettingPage like this. Whenever the value changes _playerName will change too:
class _MainFrameState extends State<MainFrame> {
void _onItemTapped(int index) => setState(() => _selectedIndex = index);
int _selectedIndex = 0;
String _playerName = '';
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
print("Main Frame build");
List<Widget> _pages = [
PlayerPage(onTextChanged: (value) {
setState(() {
_playerName = value;
});
}),
SettingPage(text: _playerName)
];
return Scaffold(
// rest of the codes are same.
)
Since your settingPage is not in the tree, you can't really access its state because it was not created.
Whenever you change your _selectedIndex in the code below, either a new settingPage or a playerPage is inflated, so just create it already with the value it depends on, listening to it if necessary.
body:Container(
decoration: new BoxDecoration(color: Colors.red),
child:Center(child:_pages.elementAt(_selectedIndex),)),
Since you are updating the screen based on the users input in page 1 I have included a code where if the text field is empty it will show a message and if it has value that value is displayed in second page
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MainFrame(),
);
}
}
class MainFrame extends StatefulWidget {
const MainFrame({key});
#override
State<MainFrame> createState() => _MainFrameState();
}
class _MainFrameState extends State<MainFrame> {
void _onItemTapped(int index) => setState(() {
print(playerPageController.text);
_selectedIndex = index;
});
int _selectedIndex = 0;
late TextEditingController playerPageController;
late Widget settingPage;
late Widget playerPage;
late List<Widget> _pages;
#override
void initState() {
playerPageController = TextEditingController();
print("Main Frame init state");
super.initState();
}
#override
Widget build(BuildContext context) {
settingPage = Text((playerPageController.text.isEmpty)
? "Theres no content here"
: playerPageController.text);
playerPage = Center(
child: TextField(
controller: playerPageController,
autofocus: true,
));
_pages = [playerPage, settingPage];
print("Main Frame build");
return Scaffold(
body: Container(
decoration: new BoxDecoration(color: Colors.red),
child: Center(
child: _pages.elementAt(_selectedIndex),
)),
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Color(0xffC03410),
unselectedItemColor: Color(0xffE96D2F),
backgroundColor: Color(0xffF7BF51),
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.play_circle),
label: 'Player',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Setting',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
);
}
}

Why the setState() affect all the same widget?

When using the BottomNavigationBar widget, I created a list with same class as body.The class used stateful widget, but when I clicked the button to call setState() the other object has affected.
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return new _HomeState();
}
}
class _HomeState extends State<Home> {
int _currentIndex = 0;
List<PlaceHolderView> _children = [
new PlaceHolderView(currentPage: 0,),
new PlaceHolderView(currentPage: 1,),
new PlaceHolderView(currentPage: 2,),
];
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: AppBar(title: Text('Full Test'),),
body: _children[_currentIndex],
bottomNavigationBar: new BottomNavigationBar(
items: [
new BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home')),
new BottomNavigationBarItem(
icon: Icon(Icons.list), title: Text('Data')),
new BottomNavigationBarItem(
icon: Icon(Icons.person), title: Text('Profile')),
],
onTap: _changeSelectedView,
currentIndex: _currentIndex,
),
);
}
void _changeSelectedView(int index) {
setState(() {
_currentIndex = index;
});
}
}
this is the placeholder view :
class PlaceHolderView extends StatefulWidget {
PlaceHolderView({Key key, this.currentPage}) : super(key: key);
final int currentPage;
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return new _PlaceHolderVieWState();
}
}
class _PlaceHolderVieWState extends State<PlaceHolderView> {
String str = 'Click Button';
void _buttonClicked() {
setState(() {
str = 'Button Clicked';
});
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Center(
child:RaisedButton(onPressed: _buttonClicked, child: Text('${widget.currentPage} page\'s button ${str}'),),
);
}
}
when I clicked the button in one of the pages, the other pages has all changed the str. so I wonder how this happened and how to avoid this.
Well,
setState() method refreshes whole widget who is stateful,
If you want to change state of specific widget at setState(), use it as stateful, and call it in stateless widget.

Flutter: How to pass data from the parent stateful widget to one of the tabs in the BottomNavigationBar?

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