skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 the StreamBuilder never caught the stream data because it was sent right before it updated.

    Giving the data event a time buffer fixed everything:

    Future.delayed(const Duration(milliseconds: 100),
            () => _allUsersStreamcontroller.add(filteredUserList.toList()));
    

  2. 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.

      Stream<List<User>> $allUsers = userRepo.getAllUsers();
      Stream<FilterData> $filterData = ...someSteam; //Could be a stream controller, a BehaviorSubject or a ReplaySubject
    
      Stream<List<User>> $filteredUsers = Rx.combineLatest2($allUsers, $filterData, (a,b) => [a,b])
        .map((data) => { // Transform data from both streams to a new stream
          List<User> users = data[0] as List<User>;
          FilterData filterData = data[1] as filterData;
    
          return users.where((x) => x.prop1 == filterData.prop1 && x.prop2 == filterData.prop2); //Return filtered users to stream
    
        });
    
    

    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 change CombineLatest emits a new value and so does the $fitleredUsers;

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search