How to persist value from a Future when switching between pages in Flutter? - flutter

I am trying to add a widget to the screen when the future has data in Screen 1.
class UserChoiceBooks extends StatelessWidget {
final String title;
UserChoiceBooks({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Provider.of<Books>(context, listen: false)
.getRecommendedBooks("test"),
builder: (ctx, snapshot) {
// Checking if future is resolved
if (snapshot.connectionState == ConnectionState.done) {
// If we got an error
if (snapshot.hasError) {
return Center(
child: Text(
'${snapshot.error} occured',
style: TextStyle(fontSize: 18),
),
);
// if we got our data
} else if (snapshot.hasData) {
// Extracting data from snapshot object
final List<Book> recommendedBooksML = snapshot.data;
return BookList(title, recommendedBooksML);
} else {
return SizedBox.shrink();
}
} else {
return Container();
}
}
// ... some code here
);
}
}
I navigate to a different screen Screen 2.
And then back to Screen 1. I loose the future data and it starts Building again.
PS:I found a temporary workaround by storing the Future data in a Global Variable
List<Book> placeholder=[];
And then setting the value from future...placeholder=snapshot.data;
When Switching between Screen 1 and 2. I am checking
placeholder==[]?UserChoiceBooks (title):BookList(title,placeholder)
Is there a better way to keep data from snapshot so that switching between screen future value is not Lost?

You are making a call during your build:
Provider.of<Books>(context, listen: false).getRecommendedBooks("test")
Thus when the screen is rebuilt, it is invoked again. You are only too lucky that no other rebuilds are happening between those two navigations, because in theory they are possible.
A recommended mindset is that a rebuild may happen at each frame, and non-rebuilding frames should be treated as a framework optimization.
So you should make a call one time and save it. There are a few options:
Create Books somewhere up the tree and pass it to the widget. Then make your widget stateful and call widget.books.getRecommendedBooks in its initState().
If Books is a singleton, you may use a service locator like https://pub.dev/packages/get_it to instantiate it once and then use GetIt.instance.get to fetch this instance anywhere in your widget. A service locator pattern has some advantages over the provider pattern.
A nasty way is to call the future in a stateful widget's state and call the method on first build if _future == null.
For stateful widgets see this tutorial: https://flutter.dev/docs/development/ui/interactive#stateful-and-stateless-widgets
Also note that you should use a key for such a widget that would be different for different titles that you pass to the constructor. Otherwise, when you replace your widget with one with another title, the framework would not know that all that follows is for another book and will not create a new state, thus initState() will not be called.

Related

How can I load Flutter text assets from a build context without reloading each build?

I'm confused about the Flutter documentation on loading text assets.
It states:
Each Flutter app has a rootBundle object for easy access to the main asset bundle. It is possible to load assets directly using the rootBundle global static from package:flutter/services.dart.
However, it’s recommended to obtain the AssetBundle for the current BuildContext using DefaultAssetBundle, rather than the default asset bundle that was built with the app; this approach enables a parent widget to substitute a different AssetBundle at run time, which can be useful for localization or testing scenarios.
I don't understand how I can implement text asset loading the recommended way, while at the same time avoiding to load the assets each time the widget is built.
Consider the following naive example that uses a FutureBuilder to display some text:
#override
Widget build(BuildContext context) {
var greeting = DefaultAssetBundle.of(context).loadString('assets/greeting');
return FutureBuilder<String>(
future: greeting,
builder (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(snapshot.requireData);
} else {
return Text('Loading greeting...');
}
}
);
}
In the example, loadString is called whenever the widget is built. This seems inefficient to me.
Also, it goes explicitly against the FutureBuilder documentation, which tells me that:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
Now, I could go ahead and load my assets in any of the recommended methods, but none of them has a BuildContext. Meaning I'd have to use the rootBundle, which wasn't recommended.
Since I'm new to Flutter, I'm unsure if the documentation is contradicting itself or if there's some obvious thing I'm missing here. Any clarification would be much appreciated.
I came up with the following solution for loading assets the way it's recommended in the Flutter docs.
Load the assets in a Widget's State and assign the Future to a nullable instance variable. This works in conjunction with FutureBuilder. Here's an example:
class MyWidgetState extends State<MyWidget> {
Future<String>? _greeting;
#override
Widget build(BuildContext context) {
// Wrapping `loadString` in a condition such as the following ensures that
// the asset is loaded no more than once. Seems kinda crude, though.
if (_greeting == null) {
_greeting = DefaultAssetBundle.of(context).loadString('assets/greeting');
}
return FutureBuilder<String>(
future: greeting,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(snapshot.requireData);
} else {
return Text('Loading greeting...');
}
});
}
}
This approach ensures that both recommendations from the Flutter docs are honored:
Assets are loaded from the Bundle for the current BuildContext.
Assets are loaded only once, avoiding that FutureBuilder restarts each build.
I would say to you... Remove Future builder.
You can do something like this:
String greeting;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
greeting =
await DefaultAssetBundle.of(context).loadString('assets/greeting');
setState(() {});
});
}
#override
Widget build(BuildContext context) {
if (greeting != null && greeting.isNotEmpty) {
return Text(greeting);
} else {
return Text('Loading greeting...');
}
}
Try this
String greeting;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => loadString());
}
void loadString() async {
if (greeting != null && greeting.isNotEmpty) {
greeting = await DefaultAssetBundle.of(context).loadString('assets/greeting');
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Text(greeting ?? 'Loading greeting...');
}
If your project has Null Safety then change String greeting to String? greeting

How to create a dependency for ChangeNotifierProvider and make it wait to complete?

I have ChangeNotifierProvider object that uses data stored sqflite asset database which need to be loaded at the beginning as future. The problem is that ChangeNotifierProvider doesn't wait for future operation to complete. I tried to add a mechanism to make ChangeNotifierProvider wait but couldn't succeed. (tried FutureBuilder, FutureProvider, using all together etc...)
Note : FutureProvider solves waiting problem but it doesn't listen the object as ChangeNotifierProvider does. When I use them in multiprovider I had two different object instances...
All solutions that I found in StackOverflow or other sites don't give a general solution or approach for this particular problem. (or I couldn't find) I believe there must be a very basic solution/approach and decided to ask for your help. How can I implement a future to this code or how can I make ChangeNotifierProvider wait for future?
Here is my summary code;
class DataSource with ChangeNotifier {
int _myId;
List _myList;
int get myId => _myId;
List get myList => _myList;
void setMyId(int changeMyId) {
_myId = changeMyId;
notifyListeners();
}
.... same setter code for myList object.
DataSource(){initDatabase();}
Future<bool> initDatabase() {
.... fetching data from asset database. (this code works properly)
return true;
}
}
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<DataSource>(
create: (context) => DataSource(),
child: MaterialApp(
home: HomePage(),
),
);
}
}
Following code and widgets has this code part (it works fine)
return Consumer<DataSource>(
builder: (context, myDataSource, child) {.......
There are multiple ways that you can achieve. The main point of it is that you should stick to reactive principle rather than trying to await the change. Say for example, you could change the state of boolean value inside the DataSource class when the ajax request changes
class DataSource extends ChangeNotifier{
bool isDone = false;
Future<bool> initDatabase(){
//Do Whatever
isDone = true;
notifyListeners();
}
}
Then you could listen to this change in the build method like so
Widget build(BuildContext ctx){
bool isDone = Provider.of<DataSource>(context).isDone;
if(isDone){
// render result
}else{
// maybe render loading
}
}

Flutter reuse state from StatefulWidget on rebuild

In short, the question is: How do I reuse the state of an entire widget subtree?
This is what my code currently looks like:
...
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Authenticating) {
return AppLoading();
} else if (state is NotAuthenticated) {
return AppOnboarding();
} else if (state is Authenticated) {
return AppMain();
} else {
return null;
}
}
),
...
Nothing fancy here, just a BlocBuilder rebuilding its child whenever the state of the underlying Bloc changes.
Now, take for instance the following state transitions: NotAuthenticated => Authenticating => NotAuthenticated because there was something wrong with the inputed information. This would result in the AppOnboarding() widget being rebuild completely from scratch with all the information lost. How can I reuse the state from the old AppOnboarding() widget to make it look like the widget was never rebuild?
What I've already tried
I've already tried using a GlobalKey for this which I would pass to the key property of my AppOnboarding widget. This is my code:
_AuthenticatingState extends State<Authenticating> {
GlobalKey<AppOnboardingState> _appOnboardingKey;
#override
initState() {
super.initState();
_appOnboardingKey = GlobalKey();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<...>(
builder: (context, state) {
...
if (state is NotAuthenticated) {
return AppOnboarding(key: _appOnboardingKey);
}
...
}
),
}
}
I was a little surprised this didn't work. Do global keys not maintain the state of the entrie widget-subtree?
Flutter runs at 60 frames per second. The key tells Flutter that some widget is the same one that existed in the last frame. If you remove some widget, even for a single frame, it will dispose the widget (call the dispose method and get rid of the widget).
In other words, the key doesn't "save" any state for later.
You have two options:
Don't dispose the widget at all.
builder: (context, state) {
return Stack(children: [
Offstage(child:AppLoading(), offstage: state is! Authenticating),
Offstage(child:AppOnboarding()), offstage: state is! NotAuthenticated),
Offstage(child:AppMain()), offstage: state is! Authenticated),
]) }
}
Save yourself that widget state, so that you can rebuild the widget later with that same information.

Flutter exception `setState() or markNeedsBuild() called during build` by calling setState in a NotificationListener callback

I'm having an hard time trying to figure out how to update a piece of a view based on a child view "event"... let me explain:
I have a screen which is composed by a Scaffold, having as a body a custom widget which calls a rest api to get the data to display (it makes use of a FutureBuilder), then it dispatch a Notification (which basically wraps the Flutter's AsynchSnapshot) that should be used in order to update the floatingActionButton (at least in my mind :P).
This is the build method of the screen:
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onMyNotification,
child: Scaffold(
appBar: MyAppBar(),
body: MyRemoteObjectView(),
floatingActionButton: MyFloatingButton(),
),
);
}
The view is rendered perfectly, the data is retrieved from the server and displayed by MyRemoteObjectView and the notification is successfully dispatched and received, BUT as soon as I call setState() in my callback, I get the exception:
setState() or markNeedsBuild() called during build.
This is the callback (defined in the same class of the build method above):
bool onMyNotification(MyNotification notification) {
AsyncSnapshot snapshot = notification.snapshot;
if (snapshot.connectionState == ConnectionState.done) {
setState(() {
// these flags are used to customize the appearance and behavior of the floating button
_serverHasBeenCalled = true;
_modelDataRetrieved = snapshot.hasData;
});
}
return true;
}
This is the point in which I send the notification (build method of MyRemoteObjectView's state):
#override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
MyNotification(snapshot).dispatch(context);
// ...
The point is: how and when should I tell Flutter to redraw the floating button (and/or other widgets)? (because of course without the setState I don't have the exception but the button is not refreshed)
Am I getting the whole thing wrong? Is there an easier way to do it? Let me know
After FutureBuilder is built, it waits for future to return a value. After it is complete, you're calling setState and then FutureBuilder would be built again and so on, resulting in infinite repaint loop.
Are you sure that you need FutureBuilder and NotificationListener in this case? You should probably do it in initState of your StatefulWidget like this:
#override
void initState() {
super.initState();
getData().then((data) {
setState(() {
_serverHasBeenCalled = true;
_modelDataRetrieved = true;
});
});
}
You can also store Future in a state and pass it to FutureBuilder.

Flutter StreamBuilder vs FutureBuilder

What is the main difference between StreamBuilder and FutureBuilder.
What to use and when to use?
What are the tasks they are intended to perform?
How each of them listens to changes in a dynamic list?
Both StreamBuilder and FutureBuilder have the same behavior: They listen to changes on their respective object. And trigger a new build when they are notified
of a new value.
So in the end, their differences are how the object they listen to works.
Future is like Promise in JS or Task in c#. They are the representation of an asynchronous request. Futures have one and only one response. A common usage of Future is to handle HTTP calls. What you can listen to on a Future is its state. Whether it's done, finished with success, or had an error. But that's it.
Stream on the other hand is like async Iterator in JS. This can be assimilated to a value that can change over time. It usually is the representation of web-sockets or events (such as clicks). By listening to a Stream you'll get each new value and also if the Stream had an error or completed.
How each of them listens to changes in a dynamic list?
A Future can't listen to a variable change. It's a one-time response. Instead, you'll need to use a Stream.
FutureBuilder is used for one time response, like taking an image from Camera, getting data once from native platform (like fetching device battery), getting file reference, making an http request etc.
On the other hand, StreamBuilder is used for fetching some data more than once, like listening for location update, playing a music, stopwatch, etc.
Here is full example mentioning both cases.
FutureBuilder solves a square value and returns the result after 5 seconds, till then we show progress indicator to the user.
StreamBuilder shows a stopwatch, incrementing _count value by 1 every second.
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _count = 0; // used by StreamBuilder
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildFutureBuilder(),
SizedBox(height: 24),
_buildStreamBuilder(),
],
),
);
}
// constructing FutureBuilder
Widget _buildFutureBuilder() {
return Center(
child: FutureBuilder<int>(
future: _calculateSquare(10),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done)
return Text("Square = ${snapshot.data}");
return CircularProgressIndicator();
},
),
);
}
// used by FutureBuilder
Future<int> _calculateSquare(int num) async {
await Future.delayed(Duration(seconds: 5));
return num * num;
}
// constructing StreamBuilder
Widget _buildStreamBuilder() {
return Center(
child: StreamBuilder<int>(
stream: _stopwatch(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active)
return Text("Stopwatch = ${snapshot.data}");
return CircularProgressIndicator();
},
),
);
}
// used by StreamBuilder
Stream<int> _stopwatch() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield _count++;
}
}
}
I find that sometimes real-world analogies work well for explaining / remembering concepts. Here's one - it's not perfect but it helps me.
Think that you are at one of those modern sushi restaurants where you have a belt going around the room with sushi boats on it. You just sit down and wait till one goes by, grab it and eat. But they also allow you to order carry out.
A Future is like the token with a number on it that they give you when you order takeout; you made the request, but the result is not yet ready but you have a placeholder. And when the result is ready, you get a callback (the digital board above the takeout counter shows your number or they shout it out) - you can now go in and grab your food (the result) to take out.
A Stream is like that belt carrying little sushi bowls. By sitting down at that table, you've "subscribed" to the stream. You don't know when the next sushi boat will arrive - but when the chef (message source) places it in the stream (belt), then the subscribers will receive it. The important thing to note is that they arrive asynchronously (you have no idea when the next boat/message will come) but they will arrive in sequence (i.e., if the chef puts three types of sushi on the belt, in some order -- you will see them come by you in that same order)
From a coding perspective -- both Futures and Streams help you deal with asynchrony (where things don't happen instantly, and you don't know when you will get a result after you make a request).
The difference is that Futures are about one-shot request/response (I ask, there is a delay, I get a notification that my Future is ready to collect, and I'm done!) whereas Streams are a continuous series of responses to a single request (I ask, there is a delay, then I keep getting responses until the stream dries up or I decide to close it and walk away).
Hope that helps.
FutureBuilder and StreamBuilder behave similarly: they listen for changes in their respective objects. In response to changing value notifications, a new build is triggered.
Ultimately, the difference lies in how they listen to async calls.
FutureBuilder
There is only one response to it. Futures are commonly used in http calls. The Future can be used to listen to the state, e.g., when it has completed fetching the data or had an error.
like as example link here.
StreamBuilder
As opposed to streams, which are iterators that can assimilate different values, which will change over time. Each new value is returned by Stream along with an error message or success message if it has any.
like as example link here.
Conclusion
The following data might help you understand the above better:
If your use case is to just get the data, and display it, like Total number of courses from a class from API. Then you can use FutureBuilder.
What if, the data updates every second or minute, while you use the app, like upcoming posts in a blog or increase comments on the blog or increase in likes on the blog. It updates asynchronously at certain interval, in that case StreamBuilder is the best option.
Bases upon the use case, you decide which one to use. Both of them are good in their own way.
Here is a full example mentioning both cases.
FutureBuilder solves a square value and returns the result after 5 seconds, till then we show a progress indicator to the user.
StreamBuilder shows a stopwatch, incrementing _count value by 1 every second.
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _count = 0; // used by StreamBuilder
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildFutureBuilder(),
SizedBox(height: 24),
_buildStreamBuilder(),
],
),
);
}
// constructing FutureBuilder
Widget _buildFutureBuilder() {
return Center(
child: FutureBuilder<int>(
future: _calculateSquare(10),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done)
return Text("Square = ${snapshot.data}");
return CircularProgressIndicator();
},
),
);
}
// used by FutureBuilder
Future<int> _calculateSquare(int num) async {
await Future.delayed(Duration(seconds: 5));
return num * num;
}
// constructing StreamBuilder
Widget _buildStreamBuilder() {
return Center(
child: StreamBuilder<int>(
stream: _stopwatch(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active)
return Text("Stopwatch = ${snapshot.data}");
return CircularProgressIndicator();
},
),
);
}
// used by StreamBuilder
Stream<int> _stopwatch() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield _count++;
}
}
}
Both StreamBuilder and FutureBuilder widgets in Flutter allow you to build reactive UIs that respond to asynchronous data changes. However, they have some differences in terms of their usage and the type of data they work with.
FutureBuilder widget is used when you want to asynchronously retrieve a single piece of data that will not change over time, such as a network request for user information. It expects a Future as its data source, and when the Future completes, it rebuilds the widget tree with the resulting data.
StreamBuilder widget, on the other hand, is used when you want to display data that can change over time, such as a real-time chat application. It expects a Stream as its data source, and whenever new data is available, it rebuilds the widget tree with the updated data.
Here are some other differences:
FutureBuilder has a single AsyncSnapshot that represents the current state of the Future, while StreamBuilder has multiple AsyncSnapshots, each representing a new piece of data emitted by the Stream.
FutureBuilder will execute the Future every time the widget is rebuilt, while StreamBuilder will only subscribe to the Stream once when the widget is mounted, and unsubscribe when the widget is disposed.
Here's an example of using FutureBuilder:
FutureBuilder<String>(
future: fetchData(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text(snapshot.data);
} else {
return CircularProgressIndicator();
}
},
);
And here's an example of using StreamBuilder:
StreamBuilder<int>(
stream: countStream(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
return Text('Count: ${snapshot.data}');
} else {
return CircularProgressIndicator();
}
},
);
In summary, FutureBuilder is used for one-time asynchronous data retrieval, while StreamBuilder is used for displaying continuously updating data.