StreamBuilder not refreshing after asyncMap future resolves

Viewed 186

I'm using the following StreamBuilder in a Stateful widget:

StreamBuilder<List<int>>(
  stream: widget.model.results(widget.type),
  builder: (context, snapshot) {
    if (!snapshot.hasData) return CircularProgressIndicator();
    if (snapshot.hasError) return Text('Error');

    final List<int> results = snapshot.data;
    return ListView.builder(
                  itemCount: results.length,
                  itemBuilder: (context, index) {
                    return _buildListTile(results[index]);
                  });
  })

And here's the bit where the Streams get built:

// inside the ViewModel
late final List<StreamController> _streamControllers = [
  StreamController<List<int>>.broadcast(),
  StreamController<List<int>>.broadcast(),
];
List<int> _results = [];

Stream<List<int>> results(int index) =>
  _streamControllers[index]
      .stream
      .debounce(Duration(milliseconds: 500))
      .asyncMap((filter) async {
    final List<int> assets = await search(filter); //  Future
    return _results..addAll(assets);
  });

The issue is that the UI doesn't get rebuilt after the search results are returned.

The debugger shows that the Future is getting resolved correctly, but that the UI doesn't get rebuilt once the result is returned (within asyncMap).

Am I using asyncMap correctly? Is there an alternative way to set this up that could potentially get it working?

EDIT: Showing the code that adds events to the stream

[0, 1].forEach((index) => 
    textController.addListener(() =>
        _streamControllers[index]
            .sink
            .add(textController[index].text));
2 Answers

U are using asyncMap correctly. Your issue might be that you add events to stream before Streambuilder starts to listen to widget.model.results(widget.type) stream.

Either use: BehaviorSubject

  final List<BehaviorSubject> _streamControllers = [
    BehaviorSubject<List<int>>(),
    BehaviorSubject<List<int>>(),
  ];

or add events AFTER widgets are built (or when we start to listen to them)

How to use onListen callback to start producing events?

You are creating a new Stream every build therefore it will be always empty and won't update correctly. You have the same controller, but asyncMap is creating a new Stream under the hood. The docs:

Creates a new stream with each data event of this stream asynchronously mapped to a new event.

The fix would be to save the instance of the stream after asyncMap is used. This can be done multiple ways. One would be to make a late initialized field inside your State.

late Stream<List<int>> myStream = widget.model.results(widget.type);

and then use this instance in the StreamBuilder:

StreamBuilder<List<int>>(
  stream: myStream,
  builder: (context, snapshot) {
    if (!snapshot.hasData) return CircularProgressIndicator();
    if (snapshot.hasError) return Text('Error');

    final List<int> results = snapshot.data;
    return ListView.builder(
                  itemCount: results.length,
                  itemBuilder: (context, index) {
                    return _buildListTile(results[index]);
                  });
  })

But you can also save the instance in initState or completely outside the widget and make Stream<List<int>> results(int index) return the saved instance or make it the list like this:

List<Stream<List<int>>> results = _streamControllers
    .map((s) => s.stream.asyncMap((filter) async {
          final List<int> assets = await search(); //  Future
          return _results..addAll(assets);
        }))
    .toList();
Related