A month of Flutter: rendering a ListView with StreamBuilder

Abraham Williams - Dec 11 '18 - - Dev Community

Originally published on bendyworks.com.

The data for Birb will be stored in Firebase Cloud Firestore. When looking at patterns to handle getting and rendering data, I decided to go with the StreamBuilder example from the 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.

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

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

The {% raw %}`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)
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .