A month of Flutter: rendering a ListView with StreamBuilder
Tags: flutter, tutorial, reactive
Published: 2018-12-11 21:07:02

_Originally published on [bendyworks.com](https://bendyworks.com/blog/a-month-of-flutter-rendering-a-list-view-with-stream-builder)._

The data for Birb will be stored in [Firebase Cloud Firestore](https://firebase.google.com/docs/firestore/). When looking at patterns to handle getting and rendering data, I decided to go with the [`StreamBuilder`](https://docs.flutter.io/flutter/widgets/StreamBuilder-class.html) example from the [cloud_firestore](https://pub.dartlang.org/packages/cloud_firestore) package documentation. I don't know how well pagination will work with this approach so that will be an experiment for another day.

The first change I'm making is turning the static `List<int>` into a `Stream`, and moving it further up the widget tree. I want the `PostsList` widget to only care about rendering items from a `Stream`, not how to create the `Stream` itself. This has the bonus of making it easier to mock data in the tests.

~~~dart
final Stream<List<int>> _posts = Stream<List<int>>.fromIterable(
  <List<int>>[
    List<int>.generate(10, (int i) => i),
  ],
);
~~~

This looks a little weird but it is basically generating a list of 10 items. That `List` of 10 items is used as the first value in a new `Stream`. From the subscription side of the `Stream`, there will be a single event with data that is a `List` of 10 items. I'm going with this pattern because Firestore will have snapshots with multiple documents.

The `PostsList` `build` method needs to be updated to consume the `Stream` and use a `StreamBuilder`:

~~~dart
@override
Widget build(BuildContext context) {
  return StreamBuilder<List<int>>(
    stream: posts,
    builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
      if (snapshot.hasError) {
        return Text('Error: ${snapshot.error}');
      }
       switch (snapshot.connectionState) {
        case ConnectionState.waiting:
          return const Text('Loading...');
        default:
          if (snapshot.data.isEmpty) {
            return const NoContent();
          }
          return _itemList(snapshot.data);
      }
    },
  );
}
~~~

The `StreamBuilder` takes a `Stream` and will then call the builder with an [`AsyncSnapshot`](https://docs.flutter.io/flutter/widgets/AsyncSnapshot-class.html). There are a couple of different states on this snapshot that need to be handled:

 - The first is checking to see if there has been an error. If there has been, render some error text.
 - Second, if the connection is waiting, show a loader while waiting for data to arrive.
 - Third, if there is no data, render the `NoContent` widget.
 - Finally if none of the previous cases are met, render the actual data.

Looking at the tests for `PostList`, all four of those scenarios are tested by creating different kinds of mock streams with the `_postsStream` helper.

~~~dart
Stream<List<int>> _postsStream(int count) {
  return Stream<List<int>>.fromIterable(
    <List<int>>[
      List<int>.generate(count, (int i) => i),
    ],
  );
}
~~~

Then I can test that the loading text is shown, followed a test that all the mocked items are rendered.

~~~dart
testWidgets('renders list of PostItems', (WidgetTester tester) async {
  // Build our app and trigger a frame.
  await tester.pumpWidget(MaterialApp(
    home: PostsList(_postsStream(5)),
  ));

  expect(find.text('Loading...'), findsOneWidget);

  await tester.pump(Duration.zero);

  expect(find.byType(PostItem), findsNWidgets(5));
});
~~~

To simulate the error case, I can throw an error on a `Future` and convert that to a stream.

~~~dart
testWidgets('renders NoContent widget', (WidgetTester tester) async {
  // Build our app and trigger a frame.
  await tester.pumpWidget(MaterialApp(
    home: PostsList(Future<List<int>>.error('Bad Connection').asStream()),
  ));

  await tester.pump(Duration.zero);
   expect(find.text('Error: Bad Connection'), findsOneWidget);
});
~~~

One other minor change I made was to reduce the height of the cards so I could be sure the additional cards were rendering.

![List of short material cards](https://thepracticaldev.s3.amazonaws.com/i/7btcsxqqu4xif69xvp2r.png)

## Code changes

- [#25 Convert PostsList to use a StreamBuilder](https://github.com/abraham/birb/pull/25)