I want to retrieve my data without using the method FutureBuilder
This is my method :
Future<bool> fetchJointures(http.Client client) async {
final response = ('{"isInteresses": false}');
return compute(parseJointures, response.body);
}
bool parseJointures(String responseBody) {
final jsonParsed = json.decode(responseBody);
return jsonParsed['isInteresses'];
}
and how this example :https://flutter.io/docs/cookbook/networking/background-parsing do to display the data :
FutureBuilder<bool>(
future: fetchJointures(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return A_Widget(data : snapshot.data);
},
);
i want to retrieve and store my data in a var like this :
bool data = snapshot.data;
Finally i search how i can retrieve my data and store it in a var and not in param of a widget.
The problem you're having is caused by the fact that you probably don't have an architecture setup for your app so your state, business logic and ui code is being mixed all into one place.
What you want to do is be able to request data independently of having it tied to a FutureBuilder (I recently did the same thing). You need to firstly separate all your operations logic from your UI so you need some kind of architecture for that. There are lots of them but the two I have found most useful is:
Scoped Model. For a decent scoped model tutorial look at this
Redux (Overkill in your current situation)
As an example, this is a function in my notices_model file.
Future fetchNotices() async {
if (_notices == null || _notices.length == 0) {
_notices = await _mobileApi.getNotices();
notifyListeners();
}
}
the _notices you see there is a local variable of type List that I expose through a property. So in short.
Setup an architecture that splits your view logic from your operations / business logic
Bind to the properties in your view logic and just perform your operations normally.
Also take a look at the FlutterSamples for architecture and examples on their github
You can store data normally even when you are using a FutureBuilder. You also do not need to specify what var type you want to return. Try this:
var data;
var initialFutureData;
new FutureBuilder(
future: fetchJointures(http.Client()), // a Future<String> or null
initialData: initialFutureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Center(child: new Text('No connection...')); // error output
case ConnectionState.waiting:
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: new CircularProgressIndicator(), // waiting indicator
));
default:
if (snapshot.hasError) return Center(child: new Text('Error: ${snapshot.error}'));
initialFutureData = snapshot.data; // store data in var for fast reloading
data = snapshot.data; // store data
return A_Widget(data: snapshot.data); // return final widget when successfull
}
}),
);
Related
I want to update the header widget after the future builder bellow it gets data from DB.
Consider this simplified code
Column (
children: [
Text('Data is loading'), // I want to changed it into `Data is loaded`
FutureBuilder(
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
if (!isCacheInitialized) {
_loadDB(); // get data from db
isCacheInitialized = true;
}
return ListView.builder
},
future: storage.ready,
)
]
)
What I already tried but failed
Call setState after _loadDB
result in error
setState() called during build.
So how to change the text Data is loading into Data is loaded after FutureBuilder gets the data?
Call setState inside the _loadDB() function instead. So, before the _loadDB() function hands over to Widget Build, It would have set the data you want.
I have 2 rest API, one is for data and another one is for images. So I do:
fetch data.
loop over the data.
inside each data loop, then fetch the image.
But what happens is that all loaded image always use the last image of the list. This is how I do it:
//this is the list fetch
return
StreamBuilder(
stream: myBloc.myList,
builder: (context, AsyncSnapshot<List<Result>> snapshot){
if (snapshot.hasData) {
//build the list
return buildList(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator());
},
);
Widget buildList(AsyncSnapshot<List<Result>> snapshot) {
return GridView.builder(
itemCount: snapshot.data.length,
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
itemBuilder: (BuildContext context, int index) {
//fetch the image
myBloc.fetchImageById(snapshot.data[index].Id.toString());
return
StreamBuilder(
stream: myBloc.imageById,
builder: (context, AsyncSnapshot<dynamic> snapshotImg){
if (snapshotImg.hasData) {
return
MyPlaceholder(
imageProvider: snapshotImg.data,
title: snapshot.data[index].name,
);
} else if (snapshotImg.hasError) {
return
MyPlaceholder(
imageProvider: null,
title: snapshot.data[index].name,
);
}
return Center(child: CircularProgressIndicator());
},
);
});
}
and this is my BLoC class:
class MyBloc {
final _repository = MyRepository();
final _fetcher = PublishSubject<List<Result>>();
final _fetcherImage = PublishSubject<dynamic>();
Observable<List<Result>> get myList => _fetcher.stream;
Observable<dynamic> get myImageById => _fetcherImage.stream;
fetchResultList() async {
List<Result> result = await _repository.fetchMyList();
_fetcher.sink.add(result);
}
fetchImageById(String _id) async {
dynamic imgBinary = await _repository.fetchImageById(_id);
_fetcherImage.sink.add(imgBinary);
}
dispose() {
_fetcher.close();
_fetcherImage.close();
}
}
final categoryBloc = CategoryBloc();
Did I miss? It's not possible to have bloc Observable inside another bloc?
But what happens is all loaded image always the last image of the
list.
You have a single stream for the images in your BLoC so all corresponding StreamBuilders from your GridView's rows will update only with the last value in the snapshot(and you end up with the last image).
If you are sure you only have a few images that you want to show in your GridView, you could make the fetchImageById() method to create a stream for that particular image, keep a reference to it and then return it. The returned stream you could then pass to the row StreamBuilder instead of myBloc.imageById, this way your rows StreamBuilders will have a different data source. When the image is loaded you could add it to that specific stream(based on the id) and your row will be updated only with that specific data. Some code:
//fetch the image
Observable<dynamic> imageStream = myBloc.fetchImageById(snapshot.data[index].Id.toString());
return StreamBuilder(
stream: imageStream,
builder: (context, AsyncSnapshot<dynamic> snapshotImg){
// rest of code
In your BLoC you'll have:
Map<String, PublishSubject<dynamic>> _backingImageStreams = HashMap()
Observable<dynamic> fetchImageById(String _id) {
PublishSubject<dynamic> backingImgStream = _backingImageStreams[id];
if (backingImgStream == null) {
backingImgStream = PublishSubject<dynamic>();
_backingImageStreams[id] = backingImgStream;
}
// i'm assuming that repository.fetchImageById() returns a Future ?!
_repository.fetchImageById(_id).then((){
_fetcherImage.sink.add(imgBinary);
});
return _fetcherImage.stream;
}
In the more general case, I think you need to change your code from a StreamBuilder for a FutureBuilder. In your widget you'll have:
Widget buildList(AsyncSnapshot<List<Result>> snapshot) {
// ...
itemBuilder: (BuildContext context, int index) {
//fetch the image
Future<dynamic> imageFuture = myBloc.fetchImageById(snapshot.data[index].Id.toString());
return FutureBuilder(
future: imageFuture,
builder: (context, AsyncSnapshot<dynamic> snapshotImg){
// rest of your current code
Then you'll need to change your BLoC method fetchImageById(). As you're dealing with images you'll want to implement some sort of cache so you'll be more efficient:
to not download the same image again if you already have it(and be fast in showing it to the user)
to not load all the images at once and clutter the memory(or completely fail)
The BLoC code:
class MyBloc {
// remove the imageId observable
// A primitive and silly cache. This will only make sure we don't make extra
// requests for images if we already have the image data, BUT if the user
// scrolls the entire GridView we will also have in memory all the image data.
// This should be replaced with some sort of disk based cache or something
// that limits the amount of memory the cache uses.
final Map<String, dynamic> cache = HashMap();
FutureOr<dynamic> fetchImageById(String _id) async {
// atempt to find the image in the cache, maybe we already downloaded it
dynamic image = cache[id];
// if we found an image for this id then we can simply return it
if (image != null) {
return image;
} else {
// this is the first time we fetch the image, or the image was previously disposed from the cache and we need to get it
dynamic image = // your code to fetch the image
// put the image in the cache so we have it for future requests
cache[id] = image;
// return the downloaded image, you could also return the Future of the fetch request but you need to add it to the cache
return image;
}
}
If you just want to show the image to the user, just make your fetchImageById() to return a future from the image fetch request(but you'll make a fetch request every time the widget is built).
So I have created a BLOC structure with a Stream as given below. The Fetcher would receive changes to a list of Chatroom ids. Then using the transformer, it would add the data in the stream to a Cache map and pipe it to the output.
Now the catch here is that each Chatroom IDs will be used to create a stream instance, so subscribe to any changes in the Chatroom data. So the Cache map basically has the Chatroom ID mapped to its corresponding Stream. ChatRoomProvider is binds the bloc with the app.
class ChatRoomBloc {
// this is similar to the Streambuilder and Itemsbuilder we have in the Stories bloc
final _chatroomsFetcher = PublishSubject<String>();
final _chatroomsOutput =
BehaviorSubject<Map<String, Observable<ChatroomModel>>>();
// Getter to Stream
Observable<Map<String, Observable<ChatroomModel>>> get chatroomStream =>
_chatroomsOutput.stream;
ChatRoomBloc() {
chatRoomPath.listen((chatrooms) => chatrooms.documents
.forEach((f) => _chatroomsFetcher.sink.add(f.documentID)));
_chatroomsFetcher.stream
.transform(_chatroomsTransformer())
.pipe(_chatroomsOutput);
}
ScanStreamTransformer<String, Map<String, Observable<ChatroomModel>>>
_chatroomsTransformer() {
return ScanStreamTransformer(
(Map<String, Observable<ChatroomModel>> cache, String id, index) {
// adding the iteam to cache map
cache[id] = chatRoomInfo(id);
print('cache ${cache.toString()}');
return cache;
}, <String, Observable<ChatroomModel>>{});
}
dispose() {
_chatroomsFetcher.close();
_chatroomsOutput.close();
}
}
Observable<ChatroomModel> chatRoomInfo(String _chatrooms) {
final _chatroomInfo = PublishSubject<ChatroomModel>();
Firestore.instance
.collection('chatRooms')
.document(_chatrooms)
.snapshots()
.listen((chatroomInfo) =>
_chatroomInfo.sink.add(ChatroomModel.fromJson(chatroomInfo.data)));
dispose() {
_chatroomInfo.close();
}
return _chatroomInfo.stream;
}
Then I create a Streambuilder with a List view to list the IDs and any data from their corresponding streams as given below.
class FeedList extends StatelessWidget {
#override
Widget build(BuildContext context) {
final chatroomBloc = ChatRoomProvider.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Chat Room'),
),
body: buildList(chatroomBloc),
);
}
Widget buildList(ChatRoomBloc chatroomBloc) {
return StreamBuilder(
// Stream only top ids to display
stream: chatroomBloc.chatroomStream,
builder: (context,
AsyncSnapshot<Map<String, Observable<ChatroomModel>>> snapshot) {
if (!snapshot.hasData) { // no data yet
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, int index) {
print('index $index and ${snapshot.data}');
return buildTile(snapshot.data[index]);
},
);
});
}
Widget buildTile(Observable<ChatroomModel> chatroomInfoStream) {
return StreamBuilder(
stream: chatroomInfoStream,
builder: (context, AsyncSnapshot<ChatroomModel> chatroomSnapshot) {
if (!chatroomSnapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
print('${chatroomSnapshot.data.name}');
print('${chatroomSnapshot.data.members.toString()}');
return Column(children: [
ListTile(
title: Text('${chatroomSnapshot.data.name}'),
trailing: Column(
children: <Widget>[
Icon(Icons.comment),
],
),
),
Divider(
height: 8.0,
),
]);
});
}
}
The output I am getting is given below. The Streambuilder is stuck at CircularProgressIndicator in the buildTile method. I think it means that the instances are getting created and added in the cache map, but they are lot listening to the right instances or there is something wrong in the way I wired up the streams. Can you please help ?
I/flutter (12856): cache {H8j0EHhu2QpicgFDGXYZ: Instance of 'PublishSubject<ChatroomModel>'}
I/flutter (12856): cache {H8j0EHhu2QpicgFDGXYZ: Instance of 'PublishSubject<ChatroomModel>', QAhKYk1cfoq8N8O6WY2N: Instance of 'PublishSubject<ChatroomModel>'}
I/flutter (12856): index 0 and {H8j0EHhu2QpicgFDGXYZ: Instance of 'PublishSubject<ChatroomModel>', QAhKYk1cfoq8N8O6WY2N: Instance of 'PublishSubject<ChatroomModel>'}
I/flutter (12856): index 1 and {H8j0EHhu2QpicgFDGXYZ: Instance of 'PublishSubject<ChatroomModel>', QAhKYk1cfoq8N8O6WY2N: Instance of 'PublishSubject<ChatroomModel>'}
As a quick fix, maybe try:
final _chatroomInfo = BehaviorSubject<ChatroomModel>();
On a second note:
The code in its current state is hard to read and understand, it's unmaintainable and inefficient. I'm not sure what you are actually trying to do.
It's a bad idea to nest StreamBuilders. It will delay the display of the chat list by at least 2 frames, because every StreamBuilder renders at least one empty frame (data = null).
Listening to a stream and feeding the result into a Subject will also add delays.
If possible, try to remove all subjects. Instead, use rx operators.
The BLoC should provide a single output stream that provides all the data that is required to render the chat list.
I was wondering when I should use the future builder. For example, if I want to make an http request and show the results in a list view, as soon as you open the view, should I have to use the future builder or just build a ListViewBuilder like:
new ListView.builder(
itemCount: _features.length,
itemBuilder: (BuildContext context, int position) {
...stuff here...
}
Moreover, if I don't want to build a list view but some more complex stuff like circular charts, should I have to use the future builder?
Hope it's clear enough!
FutureBuilder removes boilerplate code.
Let's say you want to fetch some data from the backend on page launch and show a loader until data comes.
Tasks for ListBuilder:
Have two state variables, dataFromBackend and isLoadingFlag
On launch, set isLoadingFlag = true, and based on this, show loader.
Once data arrives, set data with what you get from backend and set isLoadingFlag = false (inside setState obviously)
We need to have a if-else in widget creation. If isLoadingFlag is true, show the loader else show the data. On failure, show error message.
Tasks for FutureBuilder:
Give the async task in future of Future Builder
Based on connectionState, show message (loading, active(streams), done)
Based on data(snapshot.hasError), show view
Pros of FutureBuilder
Does not use the two state variables and setState
Reactive programming (FutureBuilder will take care of updating the view on data arrival)
Example:
FutureBuilder<String>(
future: _fetchNetworkCall, // async work
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return Text('Loading....');
default:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
else
return Text('Result: ${snapshot.data}');
}
},
)
Performance impact:
I just looked into the FutureBuilder code to understand the performance impact of using this.
FutureBuilder is just a StatefulWidget whose state variable is _snapshot
Initial state is _snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
It is subscribing to future which we send via the constructor and update the state based on that.
Example:
widget.future.then<void>((T data) {
if (_activeCallbackIdentity == callbackIdentity) {
setState(() {
_snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
});
}
}, onError: (Object error) {
if (_activeCallbackIdentity == callbackIdentity) {
setState(() {
_snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error);
});
}
});
So the FutureBuilder is a wrapper/boilerplate of what we do typically, hence there should not be any performance impact.
FutureBuilder Example
When you want to rander widget after async call then use FutureBuilder()
class _DemoState extends State<Demo> {
#override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: downloadData(), // function where you call your api
builder: (BuildContext context, AsyncSnapshot<String> snapshot) { // AsyncSnapshot<Your object type>
if( snapshot.connectionState == ConnectionState.waiting){
return Center(child: Text('Please wait its loading...'));
}else{
if (snapshot.hasError)
return Center(child: Text('Error: ${snapshot.error}'));
else
return Center(child: new Text('${snapshot.data}')); // snapshot.data :- get your object which is pass from your downloadData() function
}
},
);
}
Future<String> downloadData()async{
// var response = await http.get('https://getProjectList');
return Future.value("Data download successfully"); // return your response
}
}
In future builder, it calls the future function to wait for the result, and as soon as it produces the result it calls the builder function where we build the widget.
AsyncSnapshot has 3 state:
connectionState.none = In this state future is null
connectionState.waiting = [future] is not null, but has not yet completed
connectionState.done = [future] is not null, and has completed. If the future completed successfully, the [AsyncSnapshot.data] will be set to the value to which the future completed. If it completed with an error, [AsyncSnapshot.hasError] will be true
FutureBuilder is a Widget that will help you to execute some asynchronous function and based on that function’s result your UI will update.
I listed some use cases, why you will use FutureBuilder?
If you want to render widget after async task then use it.
We can handle loading process by simply using ConnectionState.waiting
Don't need any custom error controller. Can handle error simply dataSnapshot.error != null
As we can handle async task within the builder we do not need any setState(() { _isLoading = false; });
When we use the FutureBuilder widget we need to check for future state i.e future is resolved or not and so on. There are various State as follows:
ConnectionState.none: It means that the future is null and initialData is used as defaultValue.
ConnectionState.active: It means the future is not null but it is not resolved yet.
ConnectionState.waiting: It means the future is being resolved, and we will get the result soon enough.
ConnectionState.done: It means that the future has been resolved.
A simple implementation
Here OrdersProvider is a provider class and fetchAndSetOrders() is the method of that provider class.
body: FutureBuilder(
future: Provider.of<OrdersProvider>(context, listen: false)
.fetchAndSetOrders(),
builder: (context, dataSnapshot) {
if (dataSnapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (dataSnapshot.error != null) {
return Center(
child: Text('An error occured'),
);
} else {
return Consumer<OrdersProvider>(
builder: (context, orderData, child) => ListView.builder(
itemCount: orderData.orders.length,
itemBuilder: (context, i) => OrderItem(orderData.orders[i]),
),
);
}
}
},
),
I'm obtaining JSON data over HTTP and displaying it in a ListView. Since it's HTTP, it's all async.
Here's what I'd like to do:
var index = new ListView.builder(
controller: _scrollController,
itemBuilder: (ctx, i) async {
_log.fine("loading post $i");
var p = await _posts[i];
return p == null ? new PostPreview(p) : null;
},
);
Unfortunately, this doesn't work since IndexedWidgetBuilder has to be a synchronous function. How do I use a Future to build a child for an IndexedWidgetBuilder? It doesn't seem like there's a way to wait for the future to complete synchronously.
Previously, I was loading the data into an array and the IndexedWidgetBuilder function only checked to see if the list elements existed before returning the child widget.
var index = new ListView.builder(
controller: _scrollController,
itemBuilder: (ctx, i) {
_log.fine("loading post $i");
return _posts.length > i ? new PostPreview(_posts[i]) : null;
},
);
This works, but I would like to completely separate the view from the data and asynchronously request the JSON as needed.
This also seems, in my limited experience, like it might be a common use-case. Could an async version of IndexWidgetBuilder be added to flutter?
You can wait for asynchronous computations using a FutureBuilder. I'd probably change your PostPreview to take a Future as a constructor argument and put the FutureBuilder there, but if you want to leave PostPreview as-is, here's how to modify your itemBuilder.
var index = new ListView.builder(
controller: _scrollController,
itemBuilder: (ctx, i) {
return new FutureBuilder(
future: _posts[i],
builder: (context, snapshot) {
return snapshot.connectionState == ConnectionState.done
? new PostPreview(snapshot.data)
: new Container(); // maybe a show placeholder widget?
);
},
);
The nice thing about FutureBuilder is that it takes care of the scenarios where the async request completes and your State has already been disposed.