StreamBuilder is rebuild whenever it get new event. This cause problem with for example navigation (Navigator.push) because if new event is receive while navigate then this trigger rebuild. Because try to navigate while widget tree still being built, this will throw error.
It is not possible to prevent rebuild to avoid this issue as required.
Suggested workaround is basically take stream from cache.
Also:
here and
here
But this mean cannot have StreamBuilder build list which constantly update if also want to provide navigation from cards on list. For example in card onPressed(). See here.
So to refresh data must use pull to refresh…
Anyone have better solution?
Or is Flutter team work on solve this limitation for example by allow prevent rebuild if card is tap by user?
UPDATE:
TL;DR Is pull to refresh only way to update data since stream in StreamBuilder must be cached to prevent it rebuilding every time new event is received?
UPDATE 2:
I have try implement cache data but my code not work:
Stream<QuerySnapshot> infoSnapshot;
fetchSnapshot() {
Stream<QuerySnapshot> infoSnapshot = Firestore.instance.collection(‘info’).where(‘available’, isEqualTo: true).snapshots();
return infoSnapshot;
}
#override
void initState() {
super.initState();
fetchSnapshot();
}
...
child: StreamBuilder(
stream: infoSnapshot,
builder: (context, snapshot) {
if(snapshot.hasData) {
return ListView.builder(
itemBuilder: (context, index) =>
build(context, snapshot.data.documents[index]),
itemCount: snapshot.data.documents.length,
);
} else {
return _emptyStateWidget();
}
UPDATE 3:
I have try use StreamController but cannot implement correct:
Stream<QuerySnapshot> infoStream;
StreamController<QuerySnapshot> infoStreamController = StreamController<QuerySnapshot>();
#override
void initState() {
super.initState();
infoStream = Firestore.instance.collection(‘info’).where(‘available’, isEqualTo: true).snapshots();
infoStreamController.addStream(infoStream);
}
…
child: StreamBuilder(
stream: infoStreamController.stream,
builder: (context, snapshot) {
UPDATE 4:
Suggestion to use _localStreamController give error:
StreamController<QuerySnapshot> _localStreamController = StreamController<QuerySnapshot>();
#override
void initState() {
super.initState();
Firestore.instance.collection(‘info’).snapshots().listen((QuerySnapshot querySnapshot) {
// if(userAdded == null) {
_localStreamController.add(querySnapshot);
// }
});
...
child: StreamBuilder(
stream: _localStreamController.stream,
builder: (context, snapshot) {
The getter 'stream' was called on null.
The method 'add' was called on
null.
It seems like the actual problem based on your comments above is that it crashes after you navigate away from the view using the stream. You have to either:
Cancel your stream controller when you navigate away so that it's not listening for any more events.
Or just don't emit any new values through the stream after navigation. Add a pause on it until you come back to the view
Update: Adding code with pseudo example
class Widget {
// Your local stream
Stream<String> _localStream;
// Value to indicate if you have navigated away
bool hasNavigated = false;
...
void init() {
// subscribe to the firebase stream
firebaseStream...listen((value){
// If this value is still false then emit the same value to the localStream
if(!hasNavigated) {
_localStream.add(value);
}
});
}
Widget build() {
return StreamBuilder(
// subscribe to the local stream NOT the firebase stream
stream: _localStream,
// handle the same way as you were before
builder: (context, snapshot) {
return YourWidgets();
}
);
}
}
Try breaking everything into widgets
Running the query should cache it even if you fully close your app(I believe only cache it on fully closed for up to 30 minutes but if you remain without internet connection, you still have access to past previous cached queries from Firestore)
Try something like this:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Please work')),
body: _buildStream(context),
);
}
Widget _buildStream(BuildContext context) {
return StreamBuilder(
stream: yourFireStoreStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildAnotherwidget(context, snapshot.data.documents);
},
);
}
Widget _buildAnotherwidget(Buildcontext context, List<DocumentSnapshot> snaps){
return ListView.Builder(
itemCount: snaps.length,
itemBuilder:(context, index) {
..dostuff here...display your cards etc..or build another widget to display cards
}
);
}
focus on the breaking into more widgets. The highest part should have the streambuilder along with the stream. then go deep down into more widgets.
The streambuilder automatically will listen and subscribe to the given stream.
When streambuilder updates, it will update the lower widgets.
Now this way, when you tap on a card in a lower widget to navigate, it should not affect the highest widget because it will effect only the UI.
we placed the streambuilder in its own top level widget...
I hope i made some sense :(
I wrote the code out without testing but im sure you can get it to work
Related
I am building a Flutter app and have run into the following issue. Following the BLoC pattern, my code is structured like this:
BLoC:
class Bloc {
final _subject = BehaviourSubject<Data>;
Observable<Data> get stream => _subject.stream;
void load() async {
_subject.sink.add(Data.loading());
final data = await /* ... */;
_subject.sink.add(Data.success(data));
}
}
State:
#override
Widget build(BuildContext context) {
return StreamBuilder<Data>(
stream: widget.bloc.stream,
builder: (context, snapshot) {
print(snapshot);
if (!snapshot.hasData || snapshot.data.isLoading) {
return /* loadingindicator */;
}
return /* data view */;
},
);
}
#override
void initState() {
super.initState();
widget.bloc.load();
}
On Android, everything works as expected. The build method gets called for
Data.loading() and for Data.success().
On iOS, however, I do not receive any data at all (snapshot.hasdata == false). I added a button, which on click, listens to the stream and prints
the received value. On Android, I receive the last event, as it is expected with a BehaviorSubject. On iOS, the listen method did not get called.
Button:
MaterialButton(
child: Text('Click me'),
onPressed: () {
widget.bloc.stream.listen(print); // prints Data.success on Android, nothing on iOS
},
),
Ok, I've found the problem. As #herbert pointed out to me, it was very unlikely that rxdart was the problem here.
I have looked into my code and found that my Page was being rebuilt, and because my code was looking like this, the bloc was re-instantiated. This somehow only happened on iOS and not on Android.
'page': (context) => MyPage(bloc: Bloc()),
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 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
}
}),
);
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]),
),
);
}
}
},
),