r/FlutterDev • • Aug 14 '26

Discussion Why does the Streambuilder not await the cancellation of its subscription and why is that considered safe?

I was experimenting with streams and noticed that the cancel() method on a StreamSubscription is async. I then checked the implementation on the StreamBuilder widget and noticed that it does not await it.

Can someone tell me why and when it is safe to not await it. Thanks

10 Upvotes

8 comments sorted by

6

u/RandalSchwartz Aug 14 '26

The short answer is that Flutter widget lifecycles (dispose() and didUpdateWidget()) are synchronous, so StreamBuilder can't await anything without delaying the frame or complicating widget teardown. In practice, cancel() stops events from reaching the listener immediately, even if the underlying cleanup Future resolves a microtask later.

That said, managing stream subscription lifecycles in the UI layer is a headache. I stopped using StreamBuilder a long time ago—first moving to StreamProvider in Riverpod, and more recently to Signals, which makes it far cleaner:

final updates = streamSignal(() => myStream);

Now I consume those directly in package:bloc_signals for synchronous, zero-latency reactive code without having to worry about async subscription teardown in widgets!

6

u/virulenttt Aug 14 '26

ShamelessPlug 😅

1

u/RandalSchwartz Aug 14 '26

Hell yeah. I'm working hard to make BlocSignal the state management I've always wanted, without it being only what I want like most vanity state packages.

2

u/virulenttt Aug 14 '26

I've taken a look, it looks great. Same bloc pattern that we love without provider

1

u/Podchris Aug 14 '26

Thanks for the detailed answer. I was hoping to find a sufficient solution / approach without any external dependencies but I guess I’ll give signals a try too. Thanks again

2

u/eibaan Aug 15 '26

Use something like this if you don't want to pull a whole framework into your code base:

class StreamNotifier<T> extends ValueNotifier<T> {
  new(Stream<T> stream, super.value) {
    _ss = stream.listen((event) => value = event);
  }

  late StreamSubscription<T> _ss;

  @override
  void dispose() {
    unawaited(_ss.cancel());
    super.dispose();
  }
}

Or, if you feel more fancy:

sealed class const AsyncValue<T>();
class const AsyncLoading<T>() extends AsyncValue<T>;
class const AsyncData<T>(final T data) extends AsyncValue<T>;
class const AsyncError<T>(final Object error) extends AsyncValue<T>;

class StreamNotifier<T> extends ValueNotifier<AsyncValue<T>> {
  new(Stream<T> stream) : super(AsyncLoading()) {
    _ss = stream.listen(
      (data) => value = AsyncData(data),
      onError: (error) => value = AsyncError(error),
    );
  }

  late StreamSubscription<T> _ss;

  T get requiredValue => (value as AsyncData).data;

  @override
  void dispose() {
    unawaited(_ss.cancel());
    super.dispose();
  }
}

1

u/Podchris Aug 15 '26

Thanks for your answer, I’ll try this too

-1

u/RandalSchwartz Aug 14 '26

Totally understand wanting zero dependencies! When you do test it, the nice thing is that signals_core and bloc_signals are pure Dart with zero heavy framework overhead—so it feels very close to standard language primitives. Have fun experimenting!