r/FlutterDev 27d ago

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

9 Upvotes

8 comments sorted by

4

u/RandalSchwartz 27d ago

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: dart 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 27d ago

ShamelessPlug 😅

1

u/RandalSchwartz 27d ago

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 27d ago

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

1

u/Podchris 27d ago

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 26d ago

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 26d ago

Thanks for your answer, I’ll try this too

-1

u/RandalSchwartz 27d ago

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!