When state of widget is created - flutter

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

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

Preserve state of widget in flutter even though parent widget rebuilds

I'm trying to preserve the state of widget pages when switching between widgets using BottomNavigationBar. I've read here that I can do this using IndexedStack, however, that doesn't work in my case for two reasons:
The Scaffold in which the pages are displayed gets rebuilt when switching between pages because for some, but not all, pages the Scaffold should be extended: Scaffold( extendBody: _pageIndex == 1, ...)
The pages should be built for the first time just when the page is opened for the first time and not right from the start
Here's a small example that shows that IndexStack is not working as intended because the Scaffold rebuilds:
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _pageIndex = 1;
List<Widget> _pages = [Text("hi"), Counter()];
#override
Widget build(BuildContext context) {
return Scaffold(
extendBody: _pageIndex == 1,
appBar: AppBar(),
body: IndexedStack(
children: _pages,
index: _pageIndex,
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Goto 0',),
BottomNavigationBarItem(icon: Icon(Icons.business), label: 'Goto 1',),
],
currentIndex: _pageIndex,
onTap: (int index) {
setState(() {
_pageIndex = index;
});
print("idx " + _pageIndex.toString());
},
),
);
}
}
Demo showing that the state is not preserved
This is the Counter which can be replaced by any other stateful widget:
class Counter extends StatefulWidget {
#override
_CounterState createState() => _CounterState();
}
//this part is not important, just to show that state is lost
class _CounterState extends State<Counter> {
int _count = 0;
#override
void initState() {
_count = 0;
super.initState();
}
#override
Widget build(BuildContext context) {
return Center(
child: TextButton(
child: Text("Count: " + _count.toString(), style: TextStyle(fontSize: 20),),
onPressed: () {
setState(() {
_count++;
});
},
),
);
}
}
First off, great question! The trick is to use KeyedSubtree, and conditionally render pages depending on if they have been visited yet or not.
You could adapt your code this way to achieve your desired behavior:
class Page {
const Page(this.subtreeKey, {required this.child});
final GlobalKey subtreeKey;
final Widget child;
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
var _pageIndex = 1;
final _pages = [
Page(GlobalKey(), child: Text('Hi')),
Page(GlobalKey(), child: Counter()),
];
final _builtPages = List<bool>.generate(2, (_) => false);
#override
Widget build(BuildContext context) {
return Scaffold(
extendBody: _pageIndex == 1,
appBar: AppBar(),
body: Stack(
fit: StackFit.expand,
children: _pages.map(
(page) {
return _buildPage(
_pages.indexOf(page),
page,
);
},
).toList(),
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Goto 0',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Goto 1',
),
],
currentIndex: _pageIndex,
onTap: (int index) {
setState(() {
_pageIndex = index;
});
print("idx " + _pageIndex.toString());
},
),
);
}
Widget _buildPage(
int tabIndex,
Page page,
) {
final isCurrentlySelected = tabIndex == _pageIndex;
_builtPages[tabIndex] = isCurrentlySelected || _builtPages[tabIndex];
final Widget view = KeyedSubtree(
key: page.subtreeKey,
child: _builtPages[tabIndex] ? page.child : Container(),
);
if (tabIndex == _pageIndex) {
return view;
} else {
return Offstage(child: view);
}
}
}
You should be able to modify this code to add more tabs, functionality, etc.

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

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.

Flutter scaffold updated entire page when update on appbar

So I have a scaffold with body is a list view. And I have an appbar that manage its stage. Here my appbar code :
import 'package:flutter/material.dart';
class HgAppBar extends StatefulWidget implements PreferredSizeWidget {
final String title;
final List<Widget> actions;
HgAppBar({this.title, this.actions, Key key}) : super(key: key);
#override
HgAppBarState createState() => HgAppBarState();
#override
Size get preferredSize => new Size.fromHeight(kToolbarHeight);
}
class HgAppBarState extends State<HgAppBar> {
bool _searchOpenned = false;
void openSeach() {
setState(() {
_searchOpenned = true;
});
}
void closeSearch() {
setState(() {
_searchOpenned = true;
});
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return AppBar(
title: _searchOpenned
? TextField(
decoration: InputDecoration(
filled: true,
border: null,
fillColor: Colors.white,
),
autofocus: true,
)
: Text(widget.title ?? 'No title'),
actions: _searchOpenned
? [
IconButton(
icon: Icon(Icons.close),
onPressed: () {
setState(() {
_searchOpenned = false;
});
},
)
]
: widget.actions,
);
}
}
And here my page code:
class PageSales extends StatefulWidget {
final Store<AppState> store;
final String title;
final bool usePop;
PageSales(this.store, {this.title, this.usePop = false});
#override
State<StatefulWidget> createState() => _PageSales();
}
class _PageSales extends State<PageSales> {
final appBarKey = GlobalKey<HgAppBarState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: HgAppBar(
key: appBarKey,
title: Localizations.of(context, AppLoc).text('sales_plus'),
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
appBarKey.currentState.openSeach();
},
)
],
),
body: SafeArea(
child: Column(children: <Widget>[
Expanded(
child: FireStoreListView(
snapshot: HgFirestore.instance.productCollection.snapshots(),
itemBuilder: (context, doc) {
return WidgetProductItem(
widget.store, ProductModel.fromDocument(doc));
},
),
),
]),
),
);
}
}
so the problem is when I call the openSearch, my entire scaffold get refresh (I know it because my ListView is flashing). How do I can update my appbar without refreshing entire scaffold?
I tried your code and it seems to be fine. The screen doesn't rebuild, I'm using Flutter 2.2. I suggest adding debugPrint to make sure that the screen does get rebuild, ListView flashing isn't a definite indicator that the entire screen gets rebuild.

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