How to preserve widget states in flutter, when navigating using BottomNavigationBar? - dart

I'm currently working on building a Flutter app that will preserve states when navigating from one screen, to another, and back again when utilizing BottomNavigationBar. Just like it works in the Spotify mobile application; if you have navigated down to a certain level in the navigation hierarchy on one of the main screens, changing screen via the bottom navigation bar, and later changing back to the old screen, will preserve where the user were in that hierarchy, including preservation of the state.
I have run my head against the wall, trying various different things without success.
I want to know how I can prevent the pages in pageChooser(), when toggled once the user taps the BottomNavigationBar item, from rebuilding themselves, and instead preserve the state they already found themselves in (the pages are all stateful Widgets).
import 'package:flutter/material.dart';
import './page_plan.dart';
import './page_profile.dart';
import './page_startup_namer.dart';
void main() => runApp(new Recipher());
class Recipher extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Pages();
}
}
class Pages extends StatefulWidget {
#override
createState() => new PagesState();
}
class PagesState extends State<Pages> {
int pageIndex = 0;
pageChooser() {
switch (this.pageIndex) {
case 0:
return new ProfilePage();
break;
case 1:
return new PlanPage();
break;
case 2:
return new StartUpNamerPage();
break;
default:
return new Container(
child: new Center(
child: new Text(
'No page found by page chooser.',
style: new TextStyle(fontSize: 30.0)
)
),
);
}
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: pageChooser(),
bottomNavigationBar: new BottomNavigationBar(
currentIndex: pageIndex,
onTap: (int tappedIndex) { //Toggle pageChooser and rebuild state with the index that was tapped in bottom navbar
setState(
(){ this.pageIndex = tappedIndex; }
);
},
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
title: new Text('Profile'),
icon: new Icon(Icons.account_box)
),
new BottomNavigationBarItem(
title: new Text('Plan'),
icon: new Icon(Icons.calendar_today)
),
new BottomNavigationBarItem(
title: new Text('Startup'),
icon: new Icon(Icons.alarm_on)
)
],
)
)
);
}
}

For keeping state in BottomNavigationBar, you can use IndexedStack
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
current_tab = index;
});
},
currentIndex: current_tab,
items: [
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
),
],
),
body: IndexedStack(
children: <Widget>[
PageOne(),
PageTwo(),
],
index: current_tab,
),
);
}

Late to the party, but I've got a simple solution. Use the PageView widget with the AutomaticKeepAliveClinetMixin.
The beauty of it that it doesn't load any tab until you click on it.
The page that includes the BottomNavigationBar:
var _selectedPageIndex;
List<Widget> _pages;
PageController _pageController;
#override
void initState() {
super.initState();
_selectedPageIndex = 0;
_pages = [
//The individual tabs.
];
_pageController = PageController(initialPage: _selectedPageIndex);
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
body: PageView(
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
...
currentIndex: _selectedPageIndex,
onTap: (selectedPageIndex) {
setState(() {
_selectedPageIndex = selectedPageIndex;
_pageController.jumpToPage(selectedPageIndex);
});
},
...
}
The individual tab:
class _HomeState extends State<Home> with AutomaticKeepAliveClientMixin<Home> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
//Notice the super-call here.
super.build(context);
...
}
}
I've made a video about it here.

Use AutomaticKeepAliveClientMixin to force your tab content to not be disposed.
class PersistantTab extends StatefulWidget {
#override
_PersistantTabState createState() => _PersistantTabState();
}
class _PersistantTabState extends State<PersistantTab> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
return Container();
}
// Setting to true will force the tab to never be disposed. This could be dangerous.
#override
bool get wantKeepAlive => true;
}
To make sure your tab does get disposed when it doesn't require to be persisted, make wantKeepAlive return a class variable. You must call updateKeepAlive() to update the keep alive status.
Example with dynamic keep alive:
// class PersistantTab extends StatefulWidget ...
class _PersistantTabState extends State<PersistantTab>
with AutomaticKeepAliveClientMixin {
bool keepAlive = false;
#override
void initState() {
doAsyncStuff();
}
Future doAsyncStuff() async {
keepAlive = true;
updateKeepAlive();
// Keeping alive...
await Future.delayed(Duration(seconds: 10));
keepAlive = false;
updateKeepAlive();
// Can be disposed whenever now.
}
#override
bool get wantKeepAlive => keepAlive;
#override
Widget build(BuildContext context) {
super.build();
return Container();
}
}

Instead of returning new instance every time you run pageChooser, have one instance created and return the same.
Example:
class Pages extends StatefulWidget {
#override
createState() => new PagesState();
}
class PagesState extends State<Pages> {
int pageIndex = 0;
// Create all the pages once and return same instance when required
final ProfilePage _profilePage = new ProfilePage();
final PlanPage _planPage = new PlanPage();
final StartUpNamerPage _startUpNamerPage = new StartUpNamerPage();
Widget pageChooser() {
switch (this.pageIndex) {
case 0:
return _profilePage;
break;
case 1:
return _planPage;
break;
case 2:
return _startUpNamerPage;
break;
default:
return new Container(
child: new Center(
child: new Text(
'No page found by page chooser.',
style: new TextStyle(fontSize: 30.0)
)
),
);
}
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: pageChooser(),
bottomNavigationBar: new BottomNavigationBar(
currentIndex: pageIndex,
onTap: (int tappedIndex) { //Toggle pageChooser and rebuild state with the index that was tapped in bottom navbar
setState(
(){ this.pageIndex = tappedIndex; }
);
},
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
title: new Text('Profile'),
icon: new Icon(Icons.account_box)
),
new BottomNavigationBarItem(
title: new Text('Plan'),
icon: new Icon(Icons.calendar_today)
),
new BottomNavigationBarItem(
title: new Text('Startup'),
icon: new Icon(Icons.alarm_on)
)
],
)
)
);
}
}
Or you can make use of widgets like PageView or Stack to achieve the same.
Hope that helps!

Use “IndexedStack Widget” with “Bottom Navigation Bar Widget” to keep state of Screens/pages/Widget
Provide list of Widget to IndexedStack and index of widget you want to show because IndexedStack show single widget from list at one time.
final List<Widget> _children = [
FirstClass(),
SecondClass()
];
Scaffold(
body: IndexedStack(
index: _selectedPage,
children: _children,
),
bottomNavigationBar: BottomNavigationBar(
........
........
),
);

The most convenient way I have found to do so is using PageStorage widget along with PageStorageBucket, which acts as a key value persistent layer.
Go through this article for a beautiful explanation -> https://steemit.com/utopian-io/#tensor/persisting-user-interface-state-and-building-bottom-navigation-bars-in-dart-s-flutter-framework

Do not use IndexStack Widget, because it will instantiate all the tabs together, and suppose if all the tabs are making a network request then the callbacks will be messed up the last API calling tab will probably have the control of the callback.
Use AutomaticKeepAliveClientMixin for your stateful widget it is the simplest way to achieve it without instantiating all the tabs together.
My code had interfaces that were providing the respective responses to the calling tab I implemented it the following way.
Create your stateful widget
class FollowUpsScreen extends StatefulWidget {
FollowUpsScreen();
#override
State<StatefulWidget> createState() {
return FollowUpsScreenState();
}
}
class FollowUpsScreenState extends State<FollowUpsScreen>
with AutomaticKeepAliveClientMixin<FollowUpsScreen>
implements OperationalControls {
#override
Widget build(BuildContext context) {
//do not miss this line
super.build(context);
return .....;
}
#override
bool get wantKeepAlive => true;
}

This solution is based on CupertinoTabScaffold's implementation which won't load screens unnecessary.
import 'package:flutter/material.dart';
enum MainPage { home, profile }
class BottomNavScreen extends StatefulWidget {
const BottomNavScreen({super.key});
#override
State<BottomNavScreen> createState() => _BottomNavScreenState();
}
class _BottomNavScreenState extends State<BottomNavScreen> {
var currentPage = MainPage.home;
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageSwitchingView(
currentPageIndex: MainPage.values.indexOf(currentPage),
pageCount: MainPage.values.length,
pageBuilder: _pageBuilder,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: MainPage.values.indexOf(currentPage),
onTap: (index) => setState(() => currentPage = MainPage.values[index]),
items: const [
BottomNavigationBarItem(
label: 'Home',
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Profile',
icon: Icon(Icons.account_circle),
),
],
),
);
}
Widget _pageBuilder(BuildContext context, int index) {
final page = MainPage.values[index];
switch (page) {
case MainPage.home:
return ...
case MainPage.profile:
return ...
}
}
}
/// A widget laying out multiple pages with only one active page being built
/// at a time and on stage. Off stage pages' animations are stopped.
class PageSwitchingView extends StatefulWidget {
const PageSwitchingView({
super.key,
required this.currentPageIndex,
required this.pageCount,
required this.pageBuilder,
});
final int currentPageIndex;
final int pageCount;
final IndexedWidgetBuilder pageBuilder;
#override
State<PageSwitchingView> createState() => _PageSwitchingViewState();
}
class _PageSwitchingViewState extends State<PageSwitchingView> {
final List<bool> shouldBuildPage = <bool>[];
#override
void initState() {
super.initState();
shouldBuildPage.addAll(List<bool>.filled(widget.pageCount, false));
}
#override
void didUpdateWidget(PageSwitchingView oldWidget) {
super.didUpdateWidget(oldWidget);
// Only partially invalidate the pages cache to avoid breaking the current
// behavior. We assume that the only possible change is either:
// - new pages are appended to the page list, or
// - some trailing pages are removed.
// If the above assumption is not true, some pages may lose their state.
final lengthDiff = widget.pageCount - shouldBuildPage.length;
if (lengthDiff > 0) {
shouldBuildPage.addAll(List<bool>.filled(lengthDiff, false));
} else if (lengthDiff < 0) {
shouldBuildPage.removeRange(widget.pageCount, shouldBuildPage.length);
}
}
#override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: List<Widget>.generate(widget.pageCount, (int index) {
final active = index == widget.currentPageIndex;
shouldBuildPage[index] = active || shouldBuildPage[index];
return HeroMode(
enabled: active,
child: Offstage(
offstage: !active,
child: TickerMode(
enabled: active,
child: Builder(
builder: (BuildContext context) {
return shouldBuildPage[index] ? widget.pageBuilder(context, index) : Container();
},
),
),
),
);
}),
);
}
}

proper way of preserving tabs state in bottom nav bar is by wrapping the whole tree with PageStorage() widget which takes a PageStorageBucket bucket as a required named parameter and for those tabs to which you want to preserve its state pas those respected widgets with PageStorageKey(<str_key>) then you are done !! you can see more details in this ans which i've answered few weeks back on one question : https://stackoverflow.com/a/68620032/11974847
there's other alternatives like IndexedWidget() but you should beware while using it , i've explained y we should be catious while using IndexedWidget() in the given link answer
good luck mate ..

Related

Single Instance of Widget class is used even when different variables are coded to be used

I want to use BottomNavigationBar of Flutter so for that I have created a class called BaseWidget which will be changed as the user taps the item.
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState(this.title);
}
class _BaseWidgetState extends State<BaseWidget> {
final String title;
_BaseWidgetState(this.title);
#override
Widget build(BuildContext context) {
return Center(child: Text(title));
}
}
In the above class am returning the Center widget with child as Text widget.
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
final _home = BaseWidget('Home');
final _business = BaseWidget('Business');
final _school = BaseWidget('School');
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.business), title: Text('Business')),
BottomNavigationBarItem(
icon: Icon(Icons.school), title: Text('School')),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
this.pageIndex = index;
});
}
Widget choosePager() {
switch (pageIndex) {
case 0:
return _home;
break;
case 1:
return _business;
break;
case 2:
return _school;
break;
default:
return Text('Unknown');
break;
}
}
}
Problem 1:
Whenever user taps on the BottomNavigationBarItem the text should change to the respected string passed in the BaseWidget's constructor. But it only shows Home and the rest 2 are ignored.
Problem 2:
I am planning to replace Center widget with the ListView widget to populate the list of Schools and Businesses which will be fetched from the network API in paginated way. So I don't want to reinitialise the classes again when BottomNavigationBarItem is tapped as that would result in loss of data which is already fetched. To prevent data lose I am declaring _home, _business & _school property and using these property in choosePager() method.
There are several issues with your code:
1- The real problem is that you never rebuild the BaseWidget. You construct 3 new BaseWidgets, but you only ever call the build of the _home widget, because it's the first one returned by choosePager(). Since you don't create _home, _business, _school in the HomeWidget build, no other BaseWidget can ever get built.
2- When you don't need to store any state/variables for a widget, use a Stateless widget.
3- Don't do anything in the constructor of your State. Use initState https://docs.flutter.io/flutter/widgets/State/initState.html for that instead.
4- Create widgets using const constructors when possible.
5- Widget constructor take named parameters. One of those should be the key. Use super to call the base constructor.
With that in mind, this is what the code should look like:
class BaseWidget extends StatelessWidget {
final String title;
const BaseWidget({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Text(title),
);
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('Business'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('School'),
),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
pageIndex = index;
});
}
Widget choosePager() {
Widget result;
switch (pageIndex) {
case 0:
result = BaseWidget(title: 'Home');
break;
case 1:
result = BaseWidget(title: 'Business');
break;
case 2:
result = BaseWidget(title: 'School');
break;
default:
result = Text('Unknown');
break;
}
return result;
}
}
Edit: For your example, you may want to fetch some data from the network and only use the widget to display it. In that case, create a new class (not a Widget) to fetch & hold on to the data, and use the Widget only for displaying the data.
Some sample code:
/// Silly class to fetch data
class DataClass {
static int _nextDatum = 0;
int _data;
DataClass();
Future<int> fetchData() async {
await Future.delayed(Duration(
milliseconds: 2000,
));
_data = _nextDatum++;
return _data;
}
int getData() {
return _data;
}
}
class BaseClass extends StatefulWidget {
final String title;
final DataClass data;
const BaseClass({Key key, this.title, this.data}) : super(key: key);
#override
_BaseClassState createState() => _BaseClassState();
}
class _BaseClassState extends State<BaseClass> {
String title;
DataClass data;
#override
Widget build(BuildContext context) {
String dataStr = data == null ? ' - ' : '${data.getData()}';
return Center(
child: Text(
'$title: $dataStr',
),
);
}
#override
void initState() {
super.initState();
// initState gets called only ONCE
title = widget.title;
data = widget.data;
}
#override
void didUpdateWidget(BaseClass oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data) {
data = widget.data;
}
if (widget.title != oldWidget.title) {
title = widget.title;
}
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
Map<String, DataClass> _dataMap = <String, DataClass>{};
#override
void initState() {
super.initState();
_init().then((result) {
// Since we need to rebuild the widget with the resulting data,
// make sure to use `setState`
setState(() {
_dataMap = result;
});
});
}
Future<Map<String, DataClass>> _init() async {
// this fetches the data only once
return <String, DataClass>{
'home': DataClass()..fetchData(),
'business': DataClass()..fetchData(),
'school': DataClass()..fetchData(),
};
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('Business'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('School'),
),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
pageIndex = index;
});
}
Widget choosePager() {
Widget result;
switch (pageIndex) {
// it doesn't matter if you create a new BaseClass() a hundred times, flutter is optimized enough to not care. The `initState()` gets called only once. You're fetching the data only once.
case 0:
result = BaseClass(
title: 'Home',
data: _dataMap['home'],
);
break;
case 1:
result = BaseClass(
title: 'Business',
data: _dataMap['business'],
);
break;
case 2:
result = BaseClass(
title: 'School',
data: _dataMap['school'],
);
break;
default:
result = Text('Unknown');
break;
}
return result;
}
}
After lot of RND I solved my problem using IndexedStack. It shows the single Child from the list of children based on the index.
It will initialise all the children when the Widget build(BuildContext context) method of the _HomeWidgetState is called. So any time you switch the tabs, the object won't be reinitialised.
Here is my full code
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
#override
Widget build(BuildContext context) {
return Center(child: Text(widget.title));
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int _pageIndex = 0;
List<Widget> _children;
#override
void initState() {
super.initState();
_children = [
BaseWidget('Home'),
BaseWidget('Business'),
BaseWidget('School')
];
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: IndexedStack(
children: _children,
index: _pageIndex,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.business), title: Text('Business')),
BottomNavigationBarItem(
icon: Icon(Icons.school), title: Text('School')),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
_pageIndex = index;
});
}
}
Please try this code, I have edited the BaseWidget class
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
#override
Widget build(BuildContext context) {
return Center(child: Text(widget.title));
}
}

Passing data between pages with bottom navigation bar in flutter

I have an app that routes to a "mainapp" page after logging in. This app contains a bottom navigation bar which displays pages of the corresponding pressed icon. I want to pass data of type Map<String, dynamic> to these pages but I am having trouble. This map is generated from a function that fetches the data from a server, saves it to shared preferences, then loads the shared preferences and returns it as a map (all contained in getData()). I want to pass this map around so I don't have to load shared preferences each time, but will also update this map along with shared preferences when needed( possibly an action on one of the pages).
class MainApp extends StatefulWidget {
#override
_MainAppState createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
Map<String, dynamic> Data;
StartFunc() async {
Data = await getData();
setState(() {});
}
#override
void initState() {
StartFunc();
super.initState();
}
var _pages = [
PageOne(Data:Data),
PageTwo(),
PageThree(),
PageFour(),
PageFive(),
];
int _currentIndex = 0;
onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return _currentIndex == 2
? PageTwo()
: Scaffold(
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.library_books), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.notifications), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.add_circle_outline), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.mail), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.person), title: Text('')),
],
onTap: onTabTapped,
currentIndex: _currentIndex,
),
);
}
}
I'm getting an error saying Only static members can be accessed in initializers. I was wondering if inherited widgets or other design patterns such as scoped model and BLoC can help but not sure if that's the right way to go. I'm also not sure how I would start implementing them in this case.
There are two problems in your code:
using an async method in the body of initState()
see here for details
using instance data in an initializer
see here for details
What follow is a very basic rewrite of your code, with minimal corrections.
The data map is loaded from a mocked backend, updated inside PageOne and printed to console in PageTwo onTap callback.
Please note that I've changed instance variable Data to data to be compliant with Effective Dart guidelines.
Note that the gist does not properly addresses the synchronization of the backend service with the shared preferences: this is something that have probably to be accounted in the final product.
I just commented what it is necessary to get your code works:
if the complexity of your system and the relations with external API start growing it could be worth considering a Bloc architecture.
import 'package:flutter/material.dart';
void main() => runApp(new MainApp());
// Mock up of an async backend service
Future<Map<String, dynamic>> getData() async {
return Future.delayed(Duration(seconds: 1), () => {'prop1': 'value1'});
}
class PageOne extends StatelessWidget {
final Map<String, dynamic> data;
PageOne({Key key, this.data}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: const Text('update preferences'),
onPressed: () {
data['prop2'] = 'value2';
},
),
);
}
}
class PageTwo extends StatelessWidget {
final Map<String, dynamic> data;
PageTwo({Key key, this.data}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: const Text('Got It!'),
onPressed: () {
print("data is now: [$data]");
},
),
);
}
}
class MainApp extends StatefulWidget {
#override
_MainAppState createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
//Map<String, dynamic> Data;
Map<String, dynamic> data;
/*
StartFunc() async {
Data = await getData();
setState(() {});
}
*/
#override
void initState() {
//StartFunc();
super.initState();
getData().then((values) {
setState(() {
data = values;
});
});
}
/*
PageOne(data:data) is an invalid value for an initializer:
there is no way to access this at this point.
Initializers are executed before the constructor,
but this is only allowed to be accessed after the call
to the super constructor.
*/
/*
var _pages = [
PageOne(data:data),
PageTwo(),
];
*/
Widget getPage(int index) {
switch (index){
case 0:
return PageOne(data:data);
break;
case 1:
return PageTwo(data:data);
break;
default:
return PageOne();
break;
}
}
int _currentIndex = 0;
onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
/*
return _currentIndex == 2
? PageTwo()
: Scaffold(
I use a MaterialApp because of material widgets (RaisedButton)
It is not mandatory, but it is mainstream in flutter
*/
return MaterialApp(
title: 'My App',
home: Scaffold(
appBar: AppBar(title: Text("My App Bar")),
body: getPage(_currentIndex),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.first_page), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.last_page), title: Text('')),
],
onTap: onTabTapped,
currentIndex: _currentIndex,
),
));
}
}

Flutter list view rendering issue

I'm currently working through learning Flutter. I'm starting with trying to build the basic list app. The current flow I have is my Stateful TasksManager Widget saving user input to the state, and then pushing it to the _taskList List, which I have being sent over to a Stateless TaskList widget, which is rendering the list.
I expect the list view to be updated with the new task after the "Save" button is clicked, but what I'm getting is after I "Save", the list view only updates during the subsequent change of state. For example, if I were to "Save" the string "Foo" to add to the list, I'm only seeing that update in the view after I go to type in another item, such as "Bar".
TaskManager
class TasksManager extends StatefulWidget{
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return TasksManagerState();
}
}
class TasksManagerState extends State<TasksManager>{
final List _taskList = [];
String _newTask = '';
#override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
children: <Widget>[
TextField(
onChanged: (value){
setState(() {
_newTask = value;
});
print(_taskList);
},
),
RaisedButton(
child: Text('Save'),
onPressed: () => _taskList.add(_newTask),
),
Expanded(child: TaskList(_taskList))
],
);
}
}
TaskList
class TaskList extends StatelessWidget {
final List taskList;
TaskList(this.taskList);
Widget _buildListItem(BuildContext context, int index) {
return ListTile(
title: Text(taskList[index]),
);
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.builder(
itemBuilder: _buildListItem,
itemCount: taskList.length,
);
}
}
Yes, Flutter only updates when it is instructed to.
setState() marks the widget dirty and causes Flutter to rebuild:
onPressed: () => setState(() => _taskList.add(_newTask)),

Update UI after removing items from List

I want to update my ListView if i remove or add items. Right now i just want to delete items and see the deletion of the items immediately.
My application is more complex so i wrote a small example project to show my problems.
The TestItem class holds some data entries:
class TestItem {
static int id = 1;
bool isFinished = false;
String text;
TestItem() {
text = "Item ${id++}";
}
}
The ItemInfoViewWidget is the UI representation of the TestItem and removes the item if it is finished (whenever the Checkbox is changed to true).
class ItemInfoViewWidget extends StatefulWidget {
TestItem item;
List<TestItem> items;
ItemInfoViewWidget(this.items, this.item);
#override
_ItemInfoViewWidgetState createState() =>
_ItemInfoViewWidgetState(this.items, this.item);
}
class _ItemInfoViewWidgetState extends State<ItemInfoViewWidget> {
TestItem item;
List<TestItem> items;
_ItemInfoViewWidgetState(this.items, this.item);
#override
Widget build(BuildContext context) {
return new Card(
child: new Column(
children: <Widget>[
new Text(this.item.text),
new Checkbox(
value: this.item.isFinished, onChanged: isFinishedChanged)
],
),
);
}
void isFinishedChanged(bool value) {
setState(() {
this.item.isFinished = value;
this.items.remove(this.item);
});
}
}
The ItemViewWidget class builds the ListView.
class ItemViewWidget extends StatefulWidget {
List<TestItem> items;
ItemViewWidget(this.items);
#override
_ItemViewWidgetState createState() => _ItemViewWidgetState(this.items);
}
class _ItemViewWidgetState extends State<ItemViewWidget> {
List<TestItem> items;
_ItemViewWidgetState(this.items);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text('Test'),
),
body: ListView.builder(
itemCount: this.items.length,
itemBuilder: (BuildContext context, int index) {
return new ItemInfoViewWidget(this.items, this.items[index]);
}),
);
}
}
The MyApp shows one TestItem and a button that navigates to the ItemViewWidget page.
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> {
int _counter = 0;
List<TestItem> items = new List<TestItem>();
_MyHomePageState() {
for (int i = 0; i < 20; i++) {
this.items.add(new TestItem());
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Column(
children: <Widget>[
ItemInfoViewWidget(this.items, this.items.first),
FlatButton(
child: new Text('Open Detailed View'),
onPressed: buttonClicked,
)
],
));
}
void buttonClicked() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ItemViewWidget(this.items)),
);
}
}
If i toggle the Checkbox of the first item, the Checkbox is marked as finished (as expected), but it is not removed from the UI - however it is removed from the list.
Then I go back to the Main page and I can observe that Item 1 is checked there as well.
So if I go to the ItemViewWidget page again, I can observe that the checked items are no longer present.
Based on these observations, I come to the conclusion that my implementation works, but my UI is not updating.
How can I change my code to make an immediate update of the UI possible?
Edit: This is not a duplicate, because
I dont want to create a new instance of my list just to get the UI updated.
The answer does not work: I added this.items = List.from(this.items); but the behavior of my app is the same as already described above.
I don't want to break my reference chain by calling List.from, because my architecture has one list that is referenced by several classes. If i break the chain i have to update all references by my own. Is there a problem with my architecture?
I dont want to create a new instance of my list just to get the UI updated.
Flutter uses immutable object. Not following this rule is going against the reactive framework. It is a voluntary requirement to reduce bugs.
Fact is, this immutability is here especially to prevents developers from doing what you currently do: Having a program that depends on sharing the same instance of an object between classes; as multiple classes may want to modify it.
The real problem lies in the fact that it is your list item that removes delete an element from your list.
The thing is since it's your item which does the computing, the parent is never notified that the list changed. Therefore it doesn't know it should rerender. So nothing visually change.
To fix that you should move the deletion logic to the parent. And make sure that the parent correctly calls setState accordingly. This would translate into passing a callback to your list item, which will be called on deletion.
Here's an example:
class MyList extends StatefulWidget {
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
List<String> list = List.generate(100, (i) => i.toString());
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return MyItem(list[index], onDelete: () => removeItem(index));
},
);
}
void removeItem(int index) {
setState(() {
list = List.from(list)
..removeAt(index);
});
}
}
class MyItem extends StatelessWidget {
final String title;
final VoidCallback onDelete;
MyItem(this.title, {this.onDelete});
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(this.title),
onTap: this.onDelete,
);
}
}

How to force Flutter to rebuild / redraw all widgets?

Is there a way to force Flutter to redraw all widgets (e.g. after locale change)?
Your Widget should have a setState() method, everytime this method is called, the widget is redrawn.
Documentation : Widget setState()
Old question, but here is the solution:
In your build method, call the rebuildAllChildren function and pass it the context:
#override
Widget build(BuildContext context) {
rebuildAllChildren(context);
return ...
}
void rebuildAllChildren(BuildContext context) {
void rebuild(Element el) {
el.markNeedsBuild();
el.visitChildren(rebuild);
}
(context as Element).visitChildren(rebuild);
}
This will visit all children and mark them as needing to rebuild.
If you put this code in the topmost widget in your widgets tree, it will rebuild everything.
Also note you must order that specific widget to rebuild. Also you could have some boolean so that the rebuild of that widget only rebuilds all of its children when you really need it (it's an expensive operation, of course).
IMPORTANT: This is a hack, and you should only do this if you know what you are doing, and have strong reason to do so. One example where this is necessary is in my internationalization package: i18_extension. As Collin Jackson explained in his answer, you are really not supposed to do this in general.
This type of use case, where you have data that children can read but you don't want to explicitly pass the data to the constructor arguments of all your children, usually calls for an InheritedWidget. Flutter will automatically track which widgets depend on the data and rebuild the parts of your tree that have changed. There is a LocaleQuery widget that is designed to handle locale changes, and you can see how it's used in the Stocks example app.
Briefly, here's what Stocks is doing:
Put a callback on root widget (in this case, StocksApp) for handling locale changes. This callback does some work and then returns a customized instance of LocaleQueryData
Register this callback as the onLocaleChanged argument to the MaterialApp constructor
Child widgets that need locale information use LocaleQuery.of(context).
When the locale changes, Flutter only redraws widgets that have dependencies on the locale data.
If you want to track something other than locale changes, you can make your own class that extends InheritedWidget, and include it in the hierarchy near the root of your app. Its parent should be a StatefulWidget with key set to a GlobalKey that accessible to the children. The State of the StatefulWidget should own the data you want to distribute and expose methods for changing it that call setState. If child widgets want change the State's data, they can use the global key to get a pointer to the State (key.currentState) and call methods on it. If they want to read the data, they can call the static of(context) method of your subclass of InheritedWidget and that will tell Flutter that these widgets need to rebuilt whenever your State calls setState.
Refreshing the whole widget tree might be expensive and when you do it in front of the users eyes that wouldn't seem sweet.
so for this purpose flutter has ValueListenableBuilder<T> class. It allows you to rebuild only some of the widgets necessary for your purpose and skip the expensive widgets.
you can see the documents here ValueListenableBuilder flutter docs
or just the sample code below:
return Scaffold(
appBar: AppBar(
title: Text(widget.title)
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
ValueListenableBuilder(
builder: (BuildContext context, int value, Widget child) {
// This builder will only get called when the _counter
// is updated.
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('$value'),
child,
],
);
},
valueListenable: _counter,
// The child parameter is most helpful if the child is
// expensive to build and does not depend on the value from
// the notifier.
child: goodJob,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.plus_one),
onPressed: () => _counter.value += 1,
),
);
And also never forget the power of setState(() {});
I explain how to create a custom 'AppBuilder' widget in this post.
https://hillelcoren.com/2018/08/15/flutter-how-to-rebuild-the-entire-app-to-change-the-theme-or-locale/
You can use the widget by wrapping your MaterialApp with it, for example:
Widget build(BuildContext context) {
return AppBuilder(builder: (context) {
return MaterialApp(
...
);
});
}
You can tell the app to rebuild using:
AppBuilder.of(context).rebuild();
Simply Use:
Navigator.popAndPushNamed(context,'/screenname');
Whenever you need to refresh :)
What might work for your use case is using the Navigator to reload the page. I do this when switching between "real" and "demo" mode in my app. Here's an example :
Navigator.of(context).push(
new MaterialPageRoute(
builder: (BuildContext context){
return new SplashPage();
}
)
);
You can replace "new SplashPage()" in the above code with whatever main widget (or screen) you would like to reload. This code can be called from anywhere you have access to a BuildContext (which is most places in the UI).
Just use a Key on one of your high-level widgets, everything below this will lose state:
Key _refreshKey = UniqueKey();
void _handleLocalChanged() => setState((){
_refreshKey = UniqueKey()
});
Widget build(BuildContext context){
return MaterialApp(
key: _refreshKey ,
...
)
}
You could also use a value key like:
return MaterialApp(
key: ValueKey(locale.name)
...
);
Why not just have Flutter.redrawAllWidgetsBecauseISaidSo();? –
TimSim
There kinda is:
Change to key to redraw statefull child widgets.
Jelena Lecic explained it good enough for me on medium.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
var _forceRedraw; // generate the key from this
void _incrementCounter() {
setState(() {
_counter++;
_forceRedraw = Object();
});
}
#override
void initState() {
_forceRedraw = Object();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MyStatefullTextWidget(
key: ValueKey(_forceRedraw),
counter: _counter,
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class MyStatefullTextWidget extends StatefulWidget {
final int counter;
const MyStatefullTextWidget({
required this.counter,
Key? key,
}) : super(key: key);
#override
_MyStatefullTextWidgetState createState() => _MyStatefullTextWidgetState();
}
class _MyStatefullTextWidgetState extends State<MyStatefullTextWidget> {
#override
Widget build(BuildContext context) {
return Text(
'You have pushed the button this many times:${widget.counter}',
);
}
}
Simply Use:
Navigator.popAndPushNamed(context,'/xxx');
I my case it was enough to reconstruct the item.
Changed:
return child;
}).toList(),
To:
return SetupItemTypeButton(
type: child.type,
icon: child.icon,
active: _selected[i] == true,
onTap: ...,
);
}).toList(),
class SetupItemTypeButton extends StatelessWidget {
final dynamic type;
final String icon;
estureTapCallback onTap;
SetupItemTypeButton({Key? key, required this.type, required this.icon, required this.onTap}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container();
}
}
class SetupItemsGroup extends StatefulWidget {
final List<SetupItemTypeButton> children;
final Function(int index)? onSelect;
SetupItemsGroup({required this.children, this.onSelect});
#override
State<SetupItemsGroup> createState() => _SetupItemsGroupState();
}
class _SetupItemsGroupState extends State<SetupItemsGroup> {
final Map<int, bool> _selected = {};
#override
Widget build(BuildContext context) {
int index = 0;
return Container(
child: GridView.count(
children: widget.children.map((child) {
return SetupItemTypeButton(
type: child.type,
icon: child.icon,
active: _selected[i] == true,
onTap: () {
if (widget.onSelect != null) {
int i = index++;
child.active = _selected[i] == true;
setState(() {
_selected[i] = _selected[i] != true;
child.onTap();
widget.onSelect!(i);
});
}
},
);
}).toList(),
),
);
}
}

Resources