1

My ApplicationBloc is the root of the widget tree. In the bloc's constructor I'm listening to a stream from a repository that contains models decoded from JSON and forwarding them to another stream which is listened to by StreamBuilder.

I expected that StreamBuilder would receive models one by one and add them to AnimatedList. But there's the problem: StreamBuilder's builder fires only once with the last item in the stream.

For example, several models lay in the local storage with ids 0, 1, 2 and 3. All of these are emitted from repository, all of these are successfully put in the stream controller, but only the last model (with id == 3) appears in the AnimatedList.

Repository:

class Repository {
  static Stream<Model> load() async* {
    //...
    for (var model in models) {
      yield Model.fromJson(model);
    }
  }
}

Bloc:

class ApplicationBloc {
  ReplaySubject<Model> _outModelsController = ReplaySubject<Model>();
  Stream<Model> get outModels => _outModelsController.stream;

  ApplicationBloc() {
    TimersRepository.load().listen((model) => _outModelsController.add(model));
  }
}

main.dart:

void main() {
  runApp(
    BlocProvider<ApplicationBloc>(
      bloc: ApplicationBloc(),
      child: MyApp(),
    ),
  );
}

//...

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    final ApplicationBloc appBloc = //...

    return MaterialApp(
      //...
      body: StreamBuilder(
        stream: appBloc.outModels,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            var model = snapshot.data;
            /* inserting model to the AnimatedList */
          }

          return AnimatedList(/* ... */);
        },
      ),
    );
  }
}

Interesting notice: in the StreamBuilder's _subscribe() method onData() callback triggers required number of times but build() method fires only once.

Anton Dokusov
  • 25
  • 1
  • 4
  • as you can see `AsyncSnapshot` has `data` property which actually is `T data` and not `List data`- so it keeps only one `` element and not a list or array or some other container – pskink Dec 23 '18 at 12:49
  • @pskink It was meant this way. snapshot.data contains only one element which will be added to some list controller that stores all received items. – Anton Dokusov Dec 23 '18 at 13:04

1 Answers1

2

You need a Stream that outputs a List<Model instead of a single element. Also, listening to a stream to add it to another ReplaySubject will delay the output stream by 2 (!!!) frames, so it would be better to have a single chain.

class TimersRepository {
  // maybe use a Future if you only perform a single http request!
  static Stream<List<Model>> load() async* {
    //...
    yield models.map((json) => Model.fromJson(json)).toList();
  }
}

class ApplicationBloc {
  Stream<List<Model>> get outModels => _outModels;
  ValueConnectableObservable<List<Model>> _outModels;
  StreamSubscription _outModelsSubscription;

  ApplicationBloc() {
    // publishValue is similar to a BehaviorSubject, it always provides the latest value,
    // but without the extra delay of listening and adding to another subject
    _outModels = Observable(TimersRepository.load()).publishValue();

    // do no reload until the BLoC is disposed
    _outModelsSubscription = _outModels.connect();
  }

  void dispose() {
    // unsubcribe repo stream on dispose
    _outModelsSubscription.cancel();
  }
}

class _MyAppState extends State<MyApp> {
  ApplicationBloc _bloc;

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<Model>>(
      stream: _bloc.outModels,
      builder: (context, snapshot) {
        final models = snapshot.data ?? <Model>[];
        return ListView.builder(
          itemCount: models.length,
          itemBuilder: (context, index) => Item(model: models[index]),
        );
      },
    );
  }
}
boformer
  • 28,207
  • 10
  • 81
  • 66
  • Thank you for your response. Second `ReplaySubject` I use for delivering new models that were created at the runtime and providing seamless interface for the StreamBuilder. Yeah, of course, trick with a List will work, although it is a little unhandy. What is more significant is that I still don't know why I cannot do without it. It seems like a duct tape for me. – Anton Dokusov Dec 23 '18 at 16:11
  • 1
    If the stream would return every single list item separately (`Stream`), there would be no way for the `StreamBuilder` to know when the current list ends and the next (updated) list starts. – boformer Dec 23 '18 at 16:22
  • 2
    Think about a situation where a list item is deleted. What are you sending to your `Subject` to indicate that the item was deleted? You will have to re-send the whole updated list. – boformer Dec 23 '18 at 16:23
  • 1
    having a `Stream>` is how you do it, it's not a hack. It took me a while to learn this. It's the same on other platforms. The stream should emit a snapshot of everything that is required to render the UI. – boformer Dec 23 '18 at 16:28
  • 1
    If you are worried about performance: Yes, there will be a lot of unnecessary mapping, filtering and recalculation. That's intended. It prevents errors. And Dart is fast enough to handle. – boformer Dec 23 '18 at 16:28
  • I have a question about this solution. Say I'm receiving lost of objects and add them to the stream. Then I receive another list of objects and this time I want to add these objects on top the current stream elements, like a basic list.add() action. How can I do that? – Firat Nov 24 '19 at 18:55