Related
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 have a custom Widget called HorizontalList that receives a list and displays it horizontally.
I have made a vertical list whose rows are made of these horizontal lists.
My problem is, even if the list passed to each horizontal list is different from the others, all lines rendered are the same as if I built the widgets using the same list.
I check in the constructor of the widget itself, and the data are correct, but something goes wrong while rendering.
Widget Constructor:
HorizontalList(List<RestInfos> infos){
_rests = new List<RestInfos>();
_rests = infos;
print("horizontal list "+ _rests.length.toString());
if(_rests.length != 0)
print("horizontal list 1: " + _rests.elementAt(0).name);
}
Widget Build:
child: new ListView(
scrollDirection: Axis.horizontal,
children: new List <Widget>.generate(_rests.length, (int index) {
if(_rests.length == 0) {
//return null;
return new Card();
}
print("hotizontal " + _rests.length.toString());
print("indice "+ index.toString());
print("nome " + _rests.elementAt(index).name);
return new GestureDetector(
child:
new Card(...)
)
In the main page, I try to build the widget like this:
child: new ListView(
children: <Widget>[
new HorizontalList(list1),
new HorizontalList(list2),
new HorizontalList(list3),
new HorizontalList(list4),
]
)
I found out that the constructor i was using was "breaking" the rendering of the website.
I tried following strictly the documentation, as it is shown here and changing the code to the one below:
class HorizontalList extends StatelessWidget {
const HorizontalList({Key key, #required this.infoList}) : super(key: key);
final List<Infos> infoList;
#override
Widget build(BuildContext context) {
...
}
It's actually very strange, as I have used my old approach elsewhere in the app and didn't result in any problem.
Anyway, this solved my problem.
class MyPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: new Scaffold(
appBar: TabBar(
tabs: [
Tab(child: Text("MY INFORMATION",style: TextStyle(color: Colors.black54),)),
Tab(child: Text("WEB CALENDER",style: TextStyle(color: Colors.black54),)),
],
),
body:PersonalInformationBlocProvider(
movieBloc: PersonalInformationBloc(),
child: TabBarView(
children: [
MyInformation(),
new SmallCalendarExample(),
],
),
),
),
);
}
}
class MyInformation extends StatelessWidget{
// TODO: implement build
var deviceSize;
//Column1
Widget profileColumn(PersonalInformation snapshot) => Container(
height: deviceSize.height * 0.24,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius:
new BorderRadius.all(new Radius.circular(50.0)),
border: new Border.all(
color: Colors.black,
width: 4.0,
),
),
child: CircleAvatar(
backgroundImage: NetworkImage(
"http://www.binaythapa.com.np/img/me.jpg"),
foregroundColor: Colors.white,
backgroundColor: Colors.white,
radius: 40.0,
),
),
ProfileTile(
title: snapshot.firstName,
subtitle: "Developer",
),
SizedBox(
height: 10.0,
),
],
)
],
),
);
Widget bodyData(PersonalInformation snapshot) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
profileColumn(snapshot)
],
),
);
}
#override
Widget build(BuildContext context) {
final personalInformationBloc = PersonalInformationBlocProvider.of(context);
deviceSize = MediaQuery.of(context).size;
return StreamBuilder(
stream: personalInformationBloc.results,
builder: (context,snapshot){
if (!snapshot.hasData)
return Center(
child: CircularProgressIndicator(),
);
return bodyData(snapshot.data);
}
);
}
}
I am using Bloc Pattern for retrieving data from Rest API (just called the whole object from JSON and parsed user name only). The Page consists of two tabs MyInformation and SmallCalendar. When the app runs the data are fetched correctly and everything is good. When I go to tab two and return to tab one then the whole screens in tab one goes to red showing error:
Bad state: Stream has already been listened to.
You should use the following.
StreamController<...> _controller = StreamController<...>.broadcast();
The most common form of Stream can be listened only once at a time. If you try to add multiple listeners, it will throw
Bad state: Stream has already been listened to
To prevent this error, expose a broadcast Stream. You can convert your stream to a broadcast using myStream.asBroadcastStream
This needs to be done inside your class that expose Stream. Not as parameter of StreamBuilder. Since asBroadcastStream internally listen to the original stream to generate the broadcast one, this imply you can't call this method twice on the same stream.
You could use broadcast, which allows to listen stream more than once, but it also prevents from listening past events:
Broadcast streams do not buffer events when there is no listener.
A better option is to use BehaviorSubject from rxdart package class as StreamController. BehaviorSubject is:
A special StreamController that captures the latest item that has been added to the controller, and emits that as the first item to any new listener.
The usage is as simple as:
StreamController<...> _controller = BehaviorSubject();
In my case, I was getting this error because the same line of code myStream.listen() was being called twice in the same widget on the same stream. Apparently this is not allowed!
UPDATE:
If you intend to subscribe to the same stream more than once, you should use a behavior subject instead:
// 1- Create a behavior subject
final _myController = BehaviorSubject<String>();
// 2- To emit/broadcast new events, we will use Sink of the behavior subject.
Sink<String> get mySteamInputSink => _myController.sink;
// 3- To listen/subscribe to those emitted events, we will use Stream (observable) of the behavior subject.
Stream<String> get myStream => _myController.stream;
// 4- Firstly, Listen/subscribe to stream events.
myStream.listen((latestEvent) {
// use latestEvent data here.
});
// 5- Emit new events by adding them to the BehaviorSubject's Sink.
myStreamInputSink.add('new event');
That's it!
However, there is one final important step.
6- We must unsubscribe from all stream listeners before a widget is destroyed.
Why? (You might ask)
Because if a widget subscribes to a stream, and when this widget is destroyed, the destroyed widget stream subscription will remain in app memory causing memory leaks and unpredictable behavior.:
_flush() {
_myController.close();
_myController = StreamController<String>();
}
###############################
###############################
Old Answer:
What fixed it for me is to both create a my stream controller as a broadcast stream controller:
var myStreamController = StreamController<bool>.broadcast();
AND
use stream as a broadcast stream:
myStreamController.stream.asBroadcastStream().listen(onData);
The problem was due to not disposing the controllers in bloc.
void dispose() {
monthChangedController.close();
dayPressedController.close();
resultController.close();
}
Just to sum up:
The main difference is broadcast() creates a Stream listenable for multiple sources but it needs to be listened for at least one source to start emitting items.
A Stream should be inert until a subscriber starts listening on it (using the [onListen] callback to start producing events).
asBroadcastStream turns an existing Stream into a multi listenable one but it doesn't need to be listened to start emitting since it calls onListen() under the hood.
I have had the same issue when I used a result of Observable.combineLatest2 for StreamBuilder into Drawer:
flutter: Bad state: Stream has already been listened to.
As for me, the best solution has added the result of this combine to new BehaviorSubject and listen new one.
Don't forget to listen old one !!!
class VisitsBloc extends Object {
Map<Visit, Location> visitAndLocation;
VisitsBloc() {
visitAndLocations.listen((data) {
visitAndLocation = data;
});
}
final _newOne = new BehaviorSubject<Map<Visit, Location>>();
Stream<Map<Visit, Location>> get visitAndLocations => Observable.combineLatest2(_visits.stream, _locations.stream, (List<vis.Visit> visits, Map<int, Location> locations) {
Map<vis.Visit, Location> result = {};
visits.forEach((visit) {
if (locations.containsKey(visit.skuLocationId)) {
result[visit] = locations[visit.skuLocationId];
}
});
if (result.isNotEmpty) {
_newOne.add(result);
}
});
}
I didn't use .broadcast because it slowed my UI.
I think not all of the answers take into account the situation where you do not want or simply can't use broadcast stream.
More often than not, you have to rely on receiving past events because the listener might be created later than the stream it listens to and it's important to receive such information.
In Flutter what will often happen is that widget listening to the stream ("listener") gets destroyed and built again. If you attempt to attach listener to the same stream as before, you will get this error.
To overcome this, you will have to manage your streams manually. I created this gist demonstrating how that can be done. You can also run this code on this dartpad to see how it behaves and play with it. I have used simple String ids to refer to specific StreamController instances but there might be better solutions too (perhaps symbols).
The code from the gist is:
/* NOTE: This approach demonstrates how to recreate streams when
your listeners are being recreated.
It is useful when you cannot or do not want to use broadcast
streams. Downside to broadcast streams is that it is not
guaranteed that your listener will receive values emitted
by the stream before it was registered.
*/
import 'dart:async';
import 'dart:math';
// [StreamService] manages state of your streams. Each listener
// must have id which is used in [_streamControllers] map to
// look up relevant stream controller.
class StreamService {
final Map<String, StreamController<int>?> _streamControllers = {};
Stream<int> getNamedStream(String id) {
final controller = _getController(id);
return controller.stream;
}
// Will get existing stream controller by [id] or create a new
// one if it does not exist
StreamController<int> _getController(String id) {
final controller = _streamControllers[id] ?? _createController();
_streamControllers[id] = controller;
return controller;
}
void push(String id) {
final controller = _getController(id);
final rand = Random();
final value = rand.nextInt(1000);
controller.add(value);
}
// This method can be called by listener so
// memory leaks are avoided. This is a cleanup
// method that will make sure the stream controller
// is removed safely
void disposeController(String id) {
final controller = _streamControllers[id];
if (controller == null) {
throw Exception('Controller $id is not registered.');
}
controller.close();
_streamControllers.remove(id);
print('Removed controller $id');
}
// This method should be called when you want to remove
// all controllers. It should be called before the instance
// of this class is garbage collected / removed from memory.
void dispose() {
_streamControllers.forEach((id, controller) {
controller?.close();
print('Removed controller $id during dispose phase');
});
_streamControllers.clear();
}
StreamController<int> _createController() {
return StreamController<int>();
}
}
class ManagedListener {
ManagedListener({
required this.id,
required StreamService streamService,
}) {
_streamService = streamService;
}
final String id;
late StreamService _streamService;
StreamSubscription<int>? _subscription;
void register() {
_subscription = _streamService.getNamedStream(id).listen(_handleStreamChange);
}
void dispose() {
_subscription?.cancel();
_streamService.disposeController(id);
}
void _handleStreamChange(int n) {
print('[$id]: streamed $n');
}
}
void main(List<String> arguments) async {
final streamService = StreamService();
final listener1Id = 'id_1';
final listener2Id = 'id_2';
final listener1 = ManagedListener(id: listener1Id, streamService: streamService);
listener1.register();
streamService.push(listener1Id);
streamService.push(listener1Id);
streamService.push(listener1Id);
await Future.delayed(const Duration(seconds: 1));
final listener2 = ManagedListener(id: listener2Id, streamService: streamService);
listener2.register();
streamService.push(listener2Id);
streamService.push(listener2Id);
await Future.delayed(const Duration(seconds: 1));
listener1.dispose();
listener2.dispose();
streamService.dispose();
}
For those of you running into this while doing Future.asStream(), you'll need Future.asStream().shareReplay(maxSize: 1) to make it a broadcast/hot stream.
For me defining my stream as a global variable worked
Stream infostream (was inside the ...State in a stateful widget i defined it outside the widget and it worked
(not sure if the best solution but give it a try)
Call .broadcast() on your stream controller
example:
StreamController<T> sampleController =
StreamController<T>.broadcast();
StreamSplitter.split() from the async can be used for this use case
import 'package:async/async.dart';
...
main() {
var process = Process.start(...);
var stdout = StreamSplitter<List<int>>(process.stdout);
readStdoutFoo(stdout.split());
readStdoutBar(stdout.split());
}
readStdoutFoo(Stream<List<int>> stdout) {
stdout.transform(utf8.decoder)...
}
readStdoutBar(Stream<List<int>> stdout) {
stdout.transform(utf8.decoder)...
}
In my case I was Using the Package Connectivity while on flutter web.
Commenting all Connectivity calls solved the issue.
I'm now just using Connectivity while only on Android/iOS.
So maybe check your Packages im you are using some packages that have some issues on Web in case you are developing for web.
Hopefully I could help someone with this Information.
This is a problem for the provider, I solved it by change provider initialization
Eg
locator.registerSingleton<LoginProvider>(LoginProvider());
TO
locator.registerFactory(() => TaskProvider());
Where locator is
GetIt locator = GetIt.instance;
This could help any other person. In my case i was using two StreamBuilder one in each tab. So when i swipe to above the tab and back. The other stream was already listened so i get the error.
What i did was to remove the StreamBuilder from the tabs and put it on top. I setState each time there is a change. I return an empty Text('') to avoid showing anything. I hope this methods
For other case scenarios. Watch out if you are somehow using a stream inside a stateless class. This is one of the reasons you get the above error.
Convert the stateless class to stateful and call init and dispose method on the streamController:
#override
void initState() {
super.initState();
YourStreamController.init();
}
#override
void dispose() {
YourStreamController.dispose();
super.dispose();
}
make sure you dispose controllers!
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
I was getting this error when navigating away and then back to the view listening to the stream because I was pushing a new instance of the same view into the Navigator stack, which effectively ended up creating a new listener even though it was the same place in code.
Specifically and in more detail, I had a ListItemsView widget which uses StreamBuilder to show all the items in a stream. User taps on the "Add Item" button which pushes the AddItemView in the Navigator stack, and after submitting the form, the user is brought back to the ListItemsView, where the "Bad state: Stream has already been listened to." error happens.
For me the fix was to replace Navigator.pushNamed(context, ListItemsView.routeName) with Navigator.pop(context). This effectively prevents the instantiation of a new ListItemsView (as the second subscriber to the same stream), and just takes the user back to the previous ListItemsView instance.
I experienced this because, i was using a stream builder to create a list for tabs of tabview and anytime i switch tabs and come back to the previous i get this error. "wrapping the stream builder with a builder widget" did the magic for me.
i have experienced this, always closing the streamcontroller worked for me.
I've got a SQLite database that I query data from. From such data I create the necessary widgets and store them in a list. Passing the list to ListView.builder I create the items from the list. Everything looks good except when I add new data to the widget list. Nothing shows up if I use insert. If I use add there's no issue. Here's the snippet.
List<MessageItem> _messageItems = <MessageItem>[];
// Reads the data and creates the widgets (all OK here)
// Called when reading the data <<<
_readDBMessages() async {
List<Map> messages = await readMessages(_threadID);
List<MessageItem> holderList = <MessageItem>[];
for (final msg in messages) {
holderList.add(new MessageItem(
msg['message'],
msg['timestamp'],
msg['status'],
_contactData['photo']));
}
_messageItems.clear();
setState(() {
_messages = msges;
_messageItems = holderList;
});
}
// When I use _messageItems.add(new MessageItem()); the item shows as expected but it
// it's located on top of the list. Since my ListView is reverse
// I instead use _messagesInsert(0, new MessageItem()); in doing so
// The list is not updated. Scrolling up/down will than proceed to
// show the item as expected.
// Called when storing the data <<<
_storeNewMessage(String message) {
int timestamp = new DateTime.now().millisecondsSinceEpoch;
storeMessage(_threadID, message, _me, 'sending', timestamp, 'text').then((onValue) {
// _readDBMessages();
setState(() {
_messageItems.add(
new MessageItem(
message, timestamp, 'sending', null
)
);
print('Message inserted ---------------');
});
});
}
// Here's my listView constructor <<<
new Expanded(
child: new ListView.builder(
reverse: true,
padding: const EdgeInsets.all(12.0),
itemCount: (_messageItems == null) ? 0 : _messageItems.length,
itemBuilder: (context, i) => _messageItems[i]
)
),
According to Flutter doctor everything is OK, Thanks for the help ;-)
Perhaps you should pass a unique key to your MessageItem.
When you receive a data:
new MessageItem(
msg['message'],
msg['timestamp'],
msg['status'],
_contactData['photo'],
key: Key(msg['message']+msg['timestamp'].toString())
)
When you add a new entry:
_messageItems.add(
new MessageItem(
message, timestamp, 'sending', null, key: Key("${message}${timestamp}")
)
);
In MessageItem constructor:
MessageItem(..., {Key key}): super(key: key);
Another way is to specify a random key for the list, something like this:
child: new ListView.builder(
key: new Key(randomString()),
You can read about the keys in https://flutter.io/widgets-intro/#keys or check Dismissible widget as an example
I had a similar problem where I was listening to Floor db changes with StreamBuilder, in which the StreamBuilder was reacting but ListView was not getting updated. Tried the above solution
child: new ListView.builder(
key: UniqueKey(),
The ListView was then getting updated with new data but there was one problem, in the above implementation the ListView gets refreshed which works great with new data additions, but when I update a data in db or remove a data using dismissible widget, the ListView is again refreshed and the first element is displayed.
So I tried adding the key to each widget in the itemBuilder and it worked for me
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: array.length,
itemBuilder: (context, index) {
return ListItemWidget(
dao: dao,
item: array[index],
key: UniqueKey(),
);
},
);
Edit: I've edited the code below to feature the method that fetches the data along with the widgets that build the train estimates (replacing any API information along the way with "API_URL" and "API_STOP_ID"). I hope this even better helps us figure out the problem! I really appreciate any information anyone can give -- I've been working very hard on this project! Thank you all again!
Original post:
I have a ListView of ListTiles that each have a trailing widget which builds train arrival estimates in a new Text widget. These trailing widgets are updated every five seconds (proven by print statements). As a filler for when the app is fetching data from the train's API, it displays a "no data" Text widget which is built by _buildEstimatesNull().
However, the problem is that "no data" is still being shown even when the app has finished fetching data and _isLoading = false (proven by print statements). Still, even if that was solved, the train estimates would become quickly outdated, as the trailing widgets are updating every five seconds on their own but this would not be reflected in the actual app as the widgets were built on page load. Thus, I need a way to rebuild those trailing widgets whenever they fetch new information.
Is there a way to have Flutter automatically rebuild the ListTile's trailing widget every five seconds as well (or whenever _buildEstimatesS1 is updated / the internals of the trailing widget is updated)?
class ShuttleApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new ShuttleState();
}
}
class ShuttleState extends State<ShuttleApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new HomeState();
}
}
class HomeState extends State<HomeScreen> {
var _isLoading = true;
void initState() {
super.initState();
_fetchData();
const fiveSec = const Duration(seconds: 5);
new Timer.periodic(fiveSec, (Timer t) {
_fetchData();
});
}
var arrivalsList = new List<ArrivalEstimates>();
_fetchData() async {
arrivalsList.clear();
stopsList.clear();
final url = "API_URL";
print("Fetching: " + url);
final response = await http.get(url);
final busesJson = json.decode(response.body);
if (busesJson["service_id"] == null) {
globals.serviceActive = false;
} else {
busesJson["ResultSet"]["Result"].forEach((busJson) {
if (busJson["arrival_estimates"] != null) {
busJson["arrival_estimates"].forEach((arrivalJson) {
globals.serviceActive = true;
final arrivalEstimate = new ArrivalEstimates(
arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
);
arrivalsList.add(arrivalEstimate);
});
}
});
}
setState(() {
_isLoading = false;
});
}
Widget _buildEstimateNull() {
return new Container(
child: new Center(
child: new Text("..."),
),
);
}
Widget _buildEstimateS1() {
if (globals.serviceActive == false) {
print('serviceNotActive');
_buildEstimateNull();
} else {
final String translocStopId = "API_STOP_ID";
final estimateMatches = new List<String>();
arrivalsList.forEach((arrival) {
if (arrival.stopId == translocStopId) {
estimateMatches.add(arrival.arrivalAt);
}
});
estimateMatches.sort();
if (estimateMatches.length == 0) {
print("zero");
return _buildEstimateNull();
} else {
return new Container(
child: new Center(
child: new Text(estimateMatches[0]),
),
);
}
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: const Color(0xFF171717),
appBar: new AppBar(),
body: new DefaultTextStyle(
style: new TextStyle(color: const Color(0xFFaaaaaa),),
child: new ListView(
children: <Widget>[
new ListTile(
title: new Text('S1: Forest Hills',
style: new TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0)),
subtitle: new Text('Orange Line'),
contentPadding: new EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
trailing: _isLoading ? _buildEstimateNull() : _buildEstimateS1(),
),
],
),
)
);
}
class ArrivalEstimates {
final String routeId;
final String arrivalAt;
final String stopId;
ArrivalEstimates(this.routeId, this.arrivalAt, this.stopId);
}
Thank you so much in advance for any help you can give! I really super appreciate it! :)
There are a few ways you could tackle this. It is slightly difficult however to tell what's going on without seeing a bit more of your code - specifically how you're getting the data and what you're doing with it. But I think I can give you a sufficient answer anyways.
The simple way of doing this is to either:
Have a StatefulWidget which keeps track of the build estimates for all of the items in the list. It should request data from your API, get the results, and then call setState(() => this.listData = data);. The call to setState is what tells the widget that it needs to rebuild.
Have a StatefulWidget for each item in the list. They would all each perform an API request every 5 seconds, get the results, and then each would call setState(() => this.itemData = data);. This means multiple calls to the API etc.
The advantage of #1 is that you can batch API calls, whereas the advantage to #2 is that your build would change less overall (although the way flutter works, this would be pretty minimal)... so I would probably go with #1 if possible.
However, there is a better way of doing this!
The better way of doing this is to have some sort of API Manager (or whatever you want to call it) which handles the communication with your API. It probably would live higher up in your widget tree and would be started/stopped with whatever logic you want. Depending on how far up the widget tree is, you could either pass it into each child or more likely hold it in an InheritedWidget which could then be used to retrieve it from each list element or from the overall list.
The API manager would provide various streams - either with a bunch of named fields/methods or with a getStream(id) sort of structure depending on your API.
Then, within your various list elements, you would use StreamBuilder widgets to build each of the elements based on the data - by using a StreamBuilder you get a ConnectionState object that lets you know whether the stream has received any data yet so you can choose to show an isLoading type widget instead of the one that shows data.
By using this more advanced method, you get:
Maintainability
If your API changes, you only have to change the API manager
You can write better testing as the API interactions and the UI interactions are separated
Extensibility
If you, later on, use push notifications for updates rather than pinging a server every 5 seconds, that can be incorporated into the API manager so that it can simply update the stream without touching the UI
EDIT: as per OP's comments, they have already implemented more or less the first suggestion. However, there are a few problems with the code. I'll list them below and I've posted the code with a couple of changes.
The arrivalsList should be replaced each time a new build is done rather than simply being changed. This is because dart compares the lists and if it finds the same list, it doesn't necessarily compare all of the elements. Also, while changing it in the middle of a function isn't necessarily going to cause problems, it's generally better to use a local variable and then change the value at the end. Note that the member is actually set within setState.
If serviceActive == false, the return was missed from return _buildEstimateNull();.
Here's the code:
class HomeState extends State<HomeScreen> {
var _isLoading = true;
void initState() {
super.initState();
_fetchData();
const fiveSec = const Duration(seconds: 5);
new Timer.periodic(fiveSec, (Timer t) {
_fetchData();
});
}
var arrivalsList = new List<ArrivalEstimates>();
_fetchData() async {
var arrivalsList = new List<ArrivalEstimates>(); // *********** #1
stopsList.clear();
final url = "API_URL";
print("Fetching: " + url);
final response = await http.get(url);
final busesJson = json.decode(response.body);
if (busesJson["service_id"] == null) {
print("no service id");
globals.serviceActive = false;
} else {
busesJson["ResultSet"]["Result"].forEach((busJson) {
if (busJson["arrival_estimates"] != null) {
busJson["arrival_estimates"].forEach((arrivalJson) {
globals.serviceActive = true;
final arrivalEstimate = new ArrivalEstimates(
arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
);
arrivalsList.add(arrivalEstimate);
});
}
});
}
setState(() {
_isLoading = false;
this.arrivalsList = arrivalsList; // *********** #1
});
}
Widget _buildEstimateNull() {
return new Container(
child: new Center(
child: new Text("..."),
),
);
}
Widget _buildEstimateS1() {
if (globals.serviceActive == false) {
print('serviceNotActive');
return _buildEstimateNull(); // ************ #2
} else {
final String translocStopId = "API_STOP_ID";
final estimateMatches = new List<String>();
print("arrivalsList length: ${arrivalsList.length}");
arrivalsList.forEach((arrival) {
if (arrival.stopId == translocStopId) {
print("Estimate match found: ${arrival.stopId}");
estimateMatches.add(arrival.arrivalAt);
}
});
estimateMatches.sort();
if (estimateMatches.length == 0) {
print("zero");
return _buildEstimateNull();
} else {
return new Container(
child: new Center(
child: new Text(estimateMatches[0]),
),
);
}
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: const Color(0xFF171717),
appBar: new AppBar(),
body: new DefaultTextStyle(
style: new TextStyle(color: const Color(0xFFaaaaaa),),
child: new ListView(
children: <Widget>[
new ListTile(
title: new Text('S1: Forest Hills',
style: new TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0)),
subtitle: new Text('Orange Line'),
contentPadding: new EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
trailing: _isLoading ? _buildEstimateNull() : _buildEstimateS1(),
),
],
),
)
);
}
Instead of clearing and re-using the arrivalsList, create a new list every time the data is fetched. Otherwise Flutter is unable to detect if the list has changed.
Also, the code would clearer if you called setState whenever you change the list.
_fetchData() async {
final url = "API_URL";
print("Fetching: " + url);
final response = await http.get(url);
final busesJson = json.decode(response.body);
if (busesJson["service_id"] == null) {
globals.serviceActive = false;
setState(() {
_isLoading = false;
});
} else {
final newArrivalsList = new List<ArrivalEstimates>();
busesJson["ResultSet"]["Result"].forEach((busJson) {
if (busJson["arrival_estimates"] != null) {
busJson["arrival_estimates"].forEach((arrivalJson) {
globals.serviceActive = true;
final arrivalEstimate = new ArrivalEstimates(
arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
);
newArrivalsList.add(arrivalEstimate);
});
}
});
setState(() {
arrivalsList = newArrivalsList;
_isLoading = false;
});
}
}
A few side notes:
I'm not sure if you actually want to clear the list before you fetch the data. If the state was updated properly, that would cause a flicker every 5 seconds.
I'm not sure if you simplified the code, but calling the _fetchData method every five seconds may become a problem if the network is slow.
If you are certain that you want a child widget to rebuild every time you call setState() and it is stubbornly refusing, you can give it a UniqueKey(). This will ensure that when setState() triggers a rebuild the child widget keys will not match, the old widget will be popped and disposed of, and, the new widget will replace it in the widget tree.
Note that this is using keys in sort of the opposite way for which they were intended (to reduce rebuilding) but if something beyond your control is hindering necessary rebuilds then this is a simple, built-in way to achieve the desired goal.
Here is a very helpful Medium article on keys from one the Flutter team members, Emily Fortuna:
https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d
I am not sure if this is what your looking for but and im probably late on this but i believe you can use a change notifier efficiently to achieve this. Basically a change notifier is hooked to your backed logic() for instance an api data fetch. A widget is then registered with a change notifier of the same type as the change notifier provider. In event of data change, the widgets registered with the change notifier will be rebuild.
For instance
// extend the change notifier class
class DataClass extends ChangeNotifier {
....
getData(){
Response res = get('https://data/endpoint')
notifyListeners()
}
void onChange() {
notifyListeners();
}
....
}
Every time there is change in data you call the notifyListeners() that will trigger rebuild of consuming widgets.
Register you widget with a changenotifier
class View extends StatefulWidget {
Widget create(BuildContext context) {
return ChangeNotifierProvider<ModelClass>(
builder: (context) => DataClass(auth: auth),
child: Consumer<ModelClass>(
builder: (context, model, _) => View(model: model),
),
);
}
}
You can also user a Consumer for the same. Get more on this from the Documentation