I am loading data from Firebase and displaying it, this works perfectly fine. Now I want to let the user filter the results on the client-side. This is what doesn’t work.
I am using a StreamController
and use the add
method to send new user events. Somehow this works fine when getting data from Firebase, but not when I try to switch to the filtered results manually.
Context
Here is the normal code for the initial loading from Firebase:
class UserViewModel {
final UserRepository _userRepo;
final StreamController<List<AppUser>> _allUsersStreamcontroller =
StreamController<List<AppUser>>.broadcast();
List<AppUser> allUsersList = List.empty(growable: true);
// ... other code ...
Stream<List<AppUser>> getUserStream() {
return _allUsersStreamcontroller.stream;
}
Future<bool> loadAllUsersInRadius() async {
try {
await _userRepo.getCurrentUserLocation();
} catch (e) {
return Future.error(e);
}
// Basically load users & auto-filter some results
if (_userRepo.currentLocation != null) {
final subcription = _userRepo
.getAllUsersInRadius(radius: searchRadius)
.listen((documentList) {
final filteredUserList =
_applyAutomaticFilters(documentList, userRelationIDs);
allUsersList = filteredUserList
.map((docSnapshot) => _createUserFromSnapshot(docSnapshot))
.toList();
_allUsersStreamcontroller.add(allUsersList); // <- Important part, adding users to stream (this works)
});
subscriptions.add(subcription);
}
return true;
}
// ... Other code ...
And StreamBuilder
in the UI:
StreamBuilder<List<AppUser>>(
// Added random key because otherwise Widget didn't update reliably (suggestion from another SO question)
key: Key(Random().nextInt(10).toString()),
stream: userViewModel.getUserStream(), // <- getting stream data
builder: (context, snapshot) {
if (snapshot.hasData) {
if (_swiperEnded || (snapshot.data?.isEmpty ?? false)) {
// ... display some hint for user about empty list ...
} else {
// ... display main UI content ... <- Works fine normally
}
} else if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); // <- This is where it gets stuck after I apply manual filters
} else if (snapshot.hasError || _errorMessage != null) {
// Display error
} else {
// Unimportant
}
})
// ... other Widgets ...
// This is where the manual filters are applied, from a BottomSheet
ElevatedButton(
child: const Text("Apply"),
onPressed: () {
userVM.applyManualFilters();
setState(() {});
Navigator.pop(sheetContext, true);
})
Now here is the part where it doesn’t work. I take the user list and filter the contents based on the filter values. Then I try to add this new list to the same StreamController
.
void applyManualFilters() {
Iterable<AppUser> filteredUserList = List.from(allUsersList);
if (isTimeZoneFilterActive)
filteredUserList = _filterByTimeZoneOffset(filteredUserList);
if (isAvailabilityFilterActive)
filteredUserList = _filterByAvailability(filteredUserList);
if (isAgeFilterActive)
filteredUserList = _filterByAgeRange(filteredUserList);
_allUsersStreamcontroller.add(filteredUserList.toList()); // <- setting filtered data here, does NOT work
}
My Problem
For some reason when I add the filteredUserList
to the stream, it gets stuck on ConnectionState.waiting
and the emulator starts eating resources like crazy (laptop starts getting very loud). It seems to get stuck in an infinite loop?
What I’ve tried: I tried using two different streams instead and then switching between them in the StreamBuilder
based on a bool flag, but that ended the same.
I saw many other questions related to switching streams or similar ideas (1, 2, 3) but all the answers there sit at 0 points or don’t help at all.
I’ve seen one suggestion in this SO question to use BehaviorSubject
from RxDart which seems like it’d work, but I’d really prefer not to import a whole state management package just for this one instance.
2
Answers
After a LOT of testing, I had to find that the problem was a very basic one. The adding of new data to the stream +
setState
of my UI happened nearly at the same time, so theStreamBuilder
never caught the stream data because it was sent right before it updated.Giving the data event a time buffer fixed everything:
I’m not sure where exactly your problem is, but I’d say it is in
loadAllUsersInRadius
where you create a new subscription every time it is called.You are referring to questions about switching streams, that is not what you need. You need to combine streams. I’ll give an example below. Note that the kind of stream is not really important. They are all just streams that behave slightly differently. I’ll be using rxdart. It contains some really powerful operations on streams.
CombineLatest
does as the name suggests, combine 2 streams and emits them as a signal value. The Stream will not emit until all Streams have emitted at least one item. When either$allUsers
or$filterData
changeCombineLatest
emits a new value and so does the$fitleredUsers
;