DEV Community

Cover image for A month of Flutter: rendering a ListView with StreamBuilder
Abraham Williams
Abraham Williams

Posted on • Originally published at bendyworks.com

A month of Flutter: rendering a ListView with StreamBuilder

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

Top comments (0)