Usage of FutureBuilder with setState - dart

How to use the FutureBuilder with setState properly? For example, when i create a stateful widget its starting to load data (FutureBuilder) and then i should update the list with new data, so i use setState, but its starting to loop for infinity (because i rebuild the widget again), any solutions?
class FeedListState extends State<FeedList> {
Future<Null> updateList() async {
await widget.feeds.update();
setState(() {
widget.items = widget.feeds.getList();
});
//widget.items = widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<Null>(
future: updateList(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics:
const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: widget.items.length,
itemBuilder: (context, index) {
return FeedListItem(widget.items[index]);
},
),
onRefresh: updateList,
),
);
}
},
);
}
}

Indeed, it will loop into infinity because whenever build is called, updateList is also called and returns a brand new future.
You have to keep your build pure. It should just read and combine variables and properties, but never cause any side effects!
Another note: All fields of your StatefulWidget subclass must be final (widget.items = ... is bad). The state that changes must be stored in the State object.
In this case you can store the result (the data for the list) in the future itself, there is no need for a separate field. It's even dangerous to call setState from a future, because the future might complete after the disposal of the state, and it will throw an error.
Here is some update code that takes into account all of these things:
class FeedListState extends State<FeedList> {
// no idea how you named your data class...
Future<List<ItemData>> _listFuture;
#override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
void refreshList() {
// reload
setState(() {
_listFuture = updateAndGetList();
});
}
Future<List<ItemData>> updateAndGetList() async {
await widget.feeds.update();
// return the list here
return widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<List<ItemData>>(
future: _listFuture,
builder: (BuildContext context, AsyncSnapshot<List<ItemData>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data ?? <ItemData>[]; // handle the case that data is null
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return FeedListItem(items[index]);
},
),
onRefresh: refreshList,
),
);
}
},
);
}
}

Use can SchedulerBinding for using setState() inside Future Builders or Stream Builder,
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));

Screenshot (Null Safe):
Code:
You don't need setState while using FutureBuilder.
class MyPage extends StatefulWidget {
#override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
// Declare a variable.
late final Future<int> _future;
#override
void initState() {
super.initState();
_future = _calculate(); // Assign your Future to it.
}
// This is your actual Future.
Future<int> _calculate() => Future.delayed(Duration(seconds: 3), () => 42);
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<int>(
future: _future, // Use your variable here (not the actual Future)
builder: (_, snapshot) {
if (snapshot.hasData) return Text('Value = ${snapshot.data!}');
return Text('Loading...');
},
),
);
}
}

Related

Flutter: set parent widget state from child widget

I am very beginner to Flutter and Dart. So I am trying to update the state of the parent widget, but to be honest after trying many different solutions none worked for me, or am I doing something wrong?
What I'm trying to do is to update the _title in _BooksState() when the page changes in _Books() class.
How do I set the _title state from the child (_Books()) widget?
class Books extends StatefulWidget {
#override
_BooksState createState() {
return _BooksState();
}
}
class _BooksState extends State<Books> {
String _title = 'Books';
_setTitle(String newTitle) {
setState(() {
_title = newTitle;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
),
body: _Books(),
);
}
}
class _Books extends StatelessWidget {
final PageController _controller = PageController();
final Stream<QuerySnapshot> _stream =
Firestore.instance.collection('Books').orderBy('title').snapshots();
_setAppBarTitle(String newTitle) {
print(newTitle);
// how do I set _title from here?
}
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _stream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
final books = snapshot.data.documents;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
return PageView.builder(
controller: _controller,
scrollDirection: Axis.horizontal,
itemCount: books.length,
itemBuilder: (context, index) {
final book = books[index];
return ListTile(
title: Text(book['title']),
subtitle: Text(book['author']),
);
},
onPageChanged: (index) {
_setAppBarTitle(books[index].data['title']);
},
);
}
},
);
}
}
let me repeat your question in other words: You want to setstate a widget(or refresh a page, or change a variable 'binded' to a widget) when something happens(not inside the same class of the widget).
This is a common problem for all newbies in flutter(including me), which is called state management.
Of course you can always put everything inside the same dart file, or even the same class, But we don't do that for larger app.
In order to solve this problem, I created 2 examples:
https://github.com/lhcdims/statemanagement01
This example uses a timer to check whether something inside a widget is changed, if so, setstate the page that the widget belongs to.
try to take a look at the function funTimerDefault() inside main.dart
Ok, this was my first try, not a good solution.
https://github.com/lhcdims/statemanagement02
This example's output is the same as 1, But is using Redux instead of setState. Sooner or later you'll find that setstate is not suitable for all cases(like yours!), you'll be using Redux or BLoC.
Read the readme inside the examples, build and run them, you'll then be able to (refresh) any widget(or changes variables binded to a widget), at any time(and anywhere) you want. (even the app is pushed into background, you can also try this in the examples)
What you can do is move you _Books class inside the _BooksState class..
And instead of using _Books as class you can use it as Widget inside _BooksState class so that you can access the setState method of StatefulWidget inside the Widget you create.
I do it this way and even I'm new to Flutter and Dart...This is working for me in every case even after making an API call..I'm able to use setState and set the response from API.
Example:
class Books extends StatefulWidget {
#override
_BooksState createState() {
return _BooksState();
}
}
class _BooksState extends State<Books> {
String _title = 'Books';
_setTitle(String newTitle) {
setState(() {
_title = newTitle;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
),
body: _books(), // Using the Widget here
);
}
// Your `_Books` class created as `Widget` for setting state and new title.
Widget _books() {
final PageController _controller = PageController();
final Stream<QuerySnapshot> _stream =
Firestore.instance.collection('Books').orderBy('title').snapshots();
_setAppBarTitle(String newTitle) {
print(newTitle);
// how do I set _title from here?
// Since you created this method and setting the _title in this method
// itself using setstate you can directly pass the new title in this method..
_setTitle(newTitle);
}
return StreamBuilder<QuerySnapshot>(
stream: _stream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
final books = snapshot.data.documents;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
return PageView.builder(
controller: _controller,
scrollDirection: Axis.horizontal,
itemCount: books.length,
itemBuilder: (context, index) {
final book = books[index];
return ListTile(
title: Text(book['title']),
subtitle: Text(book['author']),
);
},
onPageChanged: (index) {
_setAppBarTitle(books[index].data['title']);
},
);
}
},
);
}
}

Flutter: Maintant state of child stateful widget

I have a listview.builder inside a stateful widget and i made a separate stateful widget for the item (ImageCard).
inside the ImageCard widget i have a like button when i click it its color change to red(like), gray(dislike).
the problem is that when i scroll down and return back the color is always gray which means that no state is saved!
how can i notify the parent stateful widget to keep the state?
Parent stateful widget
#override
Widget build(BuildContext context) {
return _buildListView(models, _scrollController);
}
Widget _buildListView(
List<PhotoModel> models, ScrollController scrollController) {
return Container(
child: ListView.builder(
controller: scrollController,
itemCount: models.length,
itemBuilder: (context, int index) {
if (index == models.length - 1) {
return SpinKitThreeBounce(
color: Colors.purple,
size: 30.0,
);
} else {
return ImageCard(
models[index].regularPhotoUrl,
models[index].mediumProfilePhotoUrl,
models[index].name,
models[index].color);
}
}));
}
child stateful widget
class ImageCard extends StatefulWidget {
final String imageUrl, userProfilePic, userName, color;
ImageCard(this.imageUrl, this.userProfilePic, this.userName, this.color);
#override
_ImageCardState createState() => _ImageCardState();
}
class _ImageCardState extends State<ImageCard> {
bool isLiked = false, isFollowing = false;
#override
Widget build(BuildContext context) {
return new Card( ....
void _onLikedBtnClicked() {
setState(() {
if (isLiked)
isLiked = false;
else {
isLiked = true;
}
});
}
Flutter will automatically disposes the widget that moves out of screen, and when they re-appear, they will be re-built rather than recovered.
So common practice is to save the state in a high-level widget, which contains at least a complete aspect of business logic and is not going to be disposed anytime soon. Then a change in the state is mapped into child widgets.
For your specific case, a simple solution is: you store the information in the parent widget, and maps them to a ImageCard inside the parent widget's build function.
Add isliked,isfollowing property to the model, then
class SomeParentState extends State<SomeParent> {
List<Model> models;
//.......
#override
Widget build(BuildContext context) {
return _buildListView(models, _scrollController);
}
Widget _buildListView(List<PhotoModel> models,
ScrollController scrollController) {
return Container(
child: ListView.builder(
controller: scrollController,
itemCount: models.length,
itemBuilder: (context, int index) {
if (index == models.length - 1) {
return SpinKitThreeBounce(
color: Colors.purple,
size: 30.0,
);
} else {
return ImageCard(
models[index].regularPhotoUrl,
models[index].mediumProfilePhotoUrl,
models[index].name,
models[index].color,
models[index].isLiked,
models[index].isFollowing,
() {
setState(() {
models[index].isLiked = !models[index].isLiked;
});
},
() {
setState(() {
models[index].isFollowing = !models[index].isFollowing;
});
},
);
}
}));
}
}
class ImageCard extends StatelessWidget{
ImageCard(
//...,
this.isLiked,
this.isFollowing,
this.likeBtnClickedListener,
this.followBtnClickedListener,
)
//...
Widget build(BuildContext context){
return Card(
//.......
IconButton(
onPressed: likeBtnClickedListener,
),
IconButton(
onPressed: followBtnClickedListener,
),
)
}
}
This should basically solve your problem. Anyway, it is easier to access and sync the data in the child widgets in this method.
If you find it easier to just keep the child widget alive, you can read the documentation of AutomaticKeepAliveClientMixin. It will stop flutter from killing this widget when it moves out of sight. But it is risky of causing memory leak.
To maintain the state of a widget inside a ListView, you need to AutomaticKeepAlive or AutomaticKeepAliveMixin (for custom widgets)
This will ensure the State instance is not destroyed when leaving the screen
ListView(
children: [
// Not kept alive
Text('Hello World'),
// kept alive
AutomaticKeepAlive(
child: Text("Hello World"),
),
]
),
You should keep your state separately then. You could make a List<bool> and have one value in there for each of the List items. You probably want to save or use the data at some point anyways, then this mechanism is going to be useless.

Flutter Programmatically trigger FutureBuilder

Let's say I have something like this:
return FutureBuilder(
future: _loadingDeals,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh,
...
)
}
)
In the _handleRefresh method, I want to programmatically trigger the re-run of the FutureBuilder.
Is there such a thing?
The use case:
When a user pulls down the refreshIndicator, then the _handleRefresh simply makes the FutureBuilder rerun itself.
Edit:
Full code snippet end to end, without the refreshing part. I've switched to using the StreamBuilder, how will the refreshIndicator part fit in all of it?
class DealList extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _DealList();
}
class _DealList extends State<DealList> with AutomaticKeepAliveClientMixin {
// prevents refreshing of tab when switch to
// Why? https://stackoverflow.com/q/51224420/1757321
bool get wantKeepAlive => true;
final RestDatasource api = new RestDatasource();
String token;
StreamController _dealsController;
#override
void initState() {
super.initState();
_dealsController = new StreamController();
_loadingDeals();
}
_loadingDeals() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
this.token = prefs.getString('token');
final res =
this.api.checkInterests(this.token).then((interestResponse) async {
_dealsController.add(interestResponse);
return interestResponse;
});
return res;
}
_handleRefresh(data) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token');
await this.api.checkInterests(token).then((interestResponse) {
_dealsController.add(interestResponse);
});
return null;
}
#override
Widget build(BuildContext context) {
super.build(context); // <-- this is with the wantKeepAlive thing
return StreamBuilder(
stream: _dealsController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
...
}
if (snapshot.connectionState != ConnectionState.done) {
return Center(
child: CircularProgressIndicator(),
);
}
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return Text('No deals');
}
if (snapshot.hasData) {
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DealsDetailPage(
dealDetail: deal,
),
),
);
},
title: Text(deal['name']),
subtitle: Text(deal['expires']),
);
},
),
}
},
);
}
}
Why not using a StreamBuilder and a Stream instead of a FutureBuilder?
Something like that...
class _YourWidgetState extends State<YourWidget> {
StreamController<String> _refreshController;
...
initState() {
super...
_refreshController = new StreamController<String>();
_loadingDeals();
}
_loadingDeals() {
_refreshController.add("");
}
_handleRefresh(data) {
if (x) _refreshController.add("");
}
...
build(context) {
...
return StreamBuilder(
stream: _refreshController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh(snapshot.data),
...
)
}
);
}
}
I created a Gist with the Flutter main example using the StreamBuilder, check it out
Using StreamBuilder is a solution, however, to trigger the FutureBuilder programmatically, just call setState, it'll rebuild the Widget.
return RefreshIndicator(
onRefresh: () {
setState(() {});
},
...
)
I prefer FutureBuilder over StreamBuilder since I am using Firestore for my project and you get billed by reads so my solution was this
_future??= getMyFuture();
shouldReload(){
setState(()=>_future = null)
}
FutureBuilder(
future: _future,
builder: (context, snapshot){
return Container();
},
)
and any user activity that needs you to get new data simply call shouldReload()

Flutter Reloading List with Streams & RxDart

I have one question regarding how to reload a list after refresh indicator is called in Flutter, using Streams and RxDart.
Here is what I have , my model class:
class HomeState {
List<Event> result;
final bool hasError;
final bool isLoading;
HomeState({
this.result,
this.hasError = false,
this.isLoading = false,
});
factory HomeState.initial() =>
new HomeState(result: new List<Event>());
factory HomeState.loading() => new HomeState(isLoading: true);
factory HomeState.error() => new HomeState(hasError: true);
}
class HomeBloc {
Stream<HomeState> state;
final EventRepository repository;
HomeBloc(this.repository) {
state = new Observable.just(new HomeState.initial());
}
void loadEvents(){
state = new Observable.fromFuture(repository.getEventList(1)).map<HomeState>((List<Event> list){
return new HomeState(
result: list,
isLoading: false
);
}).onErrorReturn(new HomeState.error())
.startWith(new HomeState.loading());
}
}
My widget:
class HomePageRx extends StatefulWidget {
#override
_HomePageRxState createState() => _HomePageRxState();
}
class _HomePageRxState extends State<HomePageRx> {
HomeBloc bloc;
_HomePageRxState() {
bloc = new HomeBloc(new EventRest());
bloc.loadEvents();
}
Future<Null> _onRefresh() async {
bloc.loadEvents();
return null;
}
#override
Widget build(BuildContext context) {
return new StreamBuilder(
stream: bloc.state,
builder: (BuildContext context, AsyncSnapshot<HomeState> snapshot) {
var state = snapshot.data;
return new Scaffold(
body: new RefreshIndicator(
onRefresh: _onRefresh,
child: new LayoutBuilder(builder:
(BuildContext context, BoxConstraints boxConstraints) {
if (state.isLoading) {
return new Center(
child: new CircularProgressIndicator(
backgroundColor: Colors.deepOrangeAccent,
strokeWidth: 5.0,
),
);
} else {
if (state.result.length > 0) {
return new ListView.builder(
itemCount: snapshot.data.result.length,
itemBuilder: (BuildContext context, int index) {
return new Text(snapshot.data.result[index].title);
});
} else {
return new Center(
child: new Text("Empty data"),
);
}
}
}),
),
);
});
}
}
The problem is when I do the pull refresh from list, the UI doesn't refresh (the server is called, the animation of the refreshindicator also), I know that the issue is related to the stream but I don't know how to solve it.
Expected result : Display the CircularProgressIndicator until the data is loaded
Any help? Thanks
You are not supposed to change the instance of state.
You should instead submit a new value to the observable. So that StreamBuilder, which is listening to state will be notified of a new value.
Which means you can't just have an Observable instance internally, as Observable doesn't have any method for adding pushing new values. So you'll need a Subject.
Basically this changes your Bloc to the following :
class HomeBloc {
final Stream<HomeState> state;
final EventRepository repository;
final Subject<HomeState> _stateSubject;
factory HomeBloc(EventRepository respository) {
final subject = new BehaviorSubject(seedValue: new HomeState.initial());
return new HomeBloc._(
repository: respository,
stateSubject: subject,
state: subject.asBroadcastStream());
}
HomeBloc._({this.state, Subject<HomeState> stateSubject, this.repository})
: _stateSubject = stateSubject;
Future<void> loadEvents() async {
_stateSubject.add(new HomeState.loading());
try {
final list = await repository.getEventList(1);
_stateSubject.add(new HomeState(result: list, isLoading: false));
} catch (err) {
_stateSubject.addError(err);
}
}
}
Also, notice how loadEvent use addError with the exception. Instead of pushing a HomeState with a hasError: true.

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

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 ..

Resources