Flutter StreamBuilder Produces Snapshots From Previous Generator Stream

452 Views Asked by At

I have a StreamBuilder wrapped in another StreamBuilder. The inner StreamBuilder loads results from a paginated asynchronous query into a ListView as they come in. The outer StreamBuilder issues a new query with user specified search text and builds a new inner StreamBuilder to listen to the new query. The query produces streams using a generator function.

I've noticed some very odd behavior: When a new query is issued and the inner StreamBuilder is rebuilt with a new Stream, it initially receives data from the previous Stream. This is particularly noticeable when the new Stream is empty (eg the user's query produced no results). If the new Stream is empty, it goes from ConnectionState.waiting to ConnectionState.done and both events are (I believe erroneously) populated with data from the previous generator Stream.

Here is some code that I wrote to reproduce this in as isolated a fashion as possible. I've replaced the outer StreamBuilder with a FutureBuilder for simplicity, though the behavior is the same.

import 'dart:async';

import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final Future<bool> oneSecondFuture = Future.delayed(Duration(seconds: 2), () {
    print('future completed!');
    return true;
  });

  @override
  Widget build(BuildContext context) {
    Stream<int> intStream(bool returnValues) async* {
      var lst = [1, 2, 3];
      var i = 0;
      while (returnValues) {
        if (i == lst.length) break;
        yield lst[i++];
        await Future.delayed(Duration(milliseconds: 40));
      }
    }

    print('building');
    var futureBuilder = FutureBuilder(
      future: oneSecondFuture,
      builder: (_, boolSnap) {
        // If the future isn't complete yet, return a StreamBuilder with a nonempty stream
        if (!boolSnap.hasData) return StreamBuilder(
            stream: intStream(true),
            builder: (_, intSnap) {
              print('Nonempty steambuilder: ' + intSnap.connectionState.toString() + '; ' + intSnap.data.toString());
              return Text(intSnap.data.toString());
            }
        );

        // If the future is complete, return a StreamBuilder with an empty stream
        return StreamBuilder(
            stream: intStream(false),
            builder: (_, intSnap) {
              print('Empty steambuilder: ' + intSnap.connectionState.toString() + '; ' + intSnap.data.toString());
              return Text(intSnap.data.toString());
            }
        );
      }
    );

    return MaterialApp(
      title: 'Test StreamBuilder',
      home: futureBuilder,
    );
  }
}

The above code produces the following print output:

I/flutter (20225): building
I/flutter (20225): Nonempty streambuilder: ConnectionState.waiting; null
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 1
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 2
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 3
I/flutter (20225): Nonempty streambuilder: ConnectionState.done; 3
I/flutter (20225): building
I/flutter (20225): Nonempty streambuilder: ConnectionState.waiting; 3
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 1
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 2
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 3
I/flutter (20225): Nonempty streambuilder: ConnectionState.done; 3
I/flutter (20225): future completed!
I/flutter (20225): Empty streambuilder: ConnectionState.waiting; 3
I/flutter (20225): Empty streambuilder: ConnectionState.done; 3

Note that the second build starts with the value '3' from the last stream while waiting for connection. The empty stream produces a value of 3 for both the waiting and the done event.

If I create a second identical intStream function (ie intStream and intStreamTwo) and call it after the future is completed, I get null data values as expected so it appears the issue stems from repeatedly calling a single generator function to get different streams:

I/flutter (20225): building
I/flutter (20225): Nonempty streambuilder: ConnectionState.waiting; null
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 1
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 2
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 3
I/flutter (20225): Nonempty streambuilder: ConnectionState.done; 3
I/flutter (20225): building
I/flutter (20225): Nonempty streambuilder: ConnectionState.waiting; 3
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 1
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 2
I/flutter (20225): Nonempty streambuilder: ConnectionState.active; 3
I/flutter (20225): Nonempty streambuilder: ConnectionState.done; 3
I/flutter (20225): future completed!
I/flutter (20225): using a different generator function:
I/flutter (20225): Empty streambuilder: ConnectionState.waiting; null
I/flutter (20225): Empty streambuilder: ConnectionState.done; null

Is this intended behavior? Is there a sane way I can get StreamBuilder to not receive data from the previous generator Stream? I suppose I could create two identical generator functions and alternate between them, but I'd really like a less hacky solution here.

1

There are 1 best solutions below

0
On

I've answered my own question by reading the documentation more closely. This appears to be (incredibly confusing) intended behavior. See documentation.

However, it's still not clear to me why using a new stream from the same generator function will trigger this behavior whereas using a new stream from a different generator function will not.

Either way, for anyone else facing the same problem, my intended work around is to just ignore the erroneous data values when the ConnectionState is 'waiting' or 'done' with some extra logic whenever a stream is empty (eg a 'done' snapshot is received without ever receiving a valid event value).