avoid_unassigned_stream_subscriptions
Flags Stream.listen() calls whose return value (a StreamSubscription) is not assigned to a variable, returned, or passed as an argument. Without storing the subscription, you have no way to cancel it later, which leads to memory leaks and unexpected behavior.
Why use this rule
Section titled “Why use this rule”A StreamSubscription that is never stored cannot be cancelled. The listener keeps running indefinitely, holding references to the callback closure and everything it captures. This is especially problematic in StatefulWidgets where the stream may outlive the widget, causing setState() calls on a disposed State.
See also: Dart - Streams | StreamSubscription | Dart lint: cancel_subscriptions
Subscribing in initState and dropping the handle. The widget is disposed, the stream keeps producing, and the callback keeps calling setState on a dead State:
import 'dart:async';
class _ChatState extends State<Chat> { final Stream<String> _messages = const Stream<String>.empty(); List<String> _received = [];
@override void initState() { super.initState(); // Nothing holds the subscription, so dispose() has nothing to cancel _messages.listen((message) { setState(() => _received = [..._received, message]); }); }
@override Widget build(BuildContext context) => const SizedBox();}Store the handle in a field and cancel it in dispose():
import 'dart:async';
class _ChatState extends State<Chat> { final Stream<String> _messages = const Stream<String>.empty(); List<String> _received = []; StreamSubscription<String>? _subscription;
@override void initState() { super.initState(); _subscription = _messages.listen((message) { setState(() => _received = [..._received, message]); }); }
@override void dispose() { _subscription?.cancel(); super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}Handing the subscription to a caller
Section titled “Handing the subscription to a caller”A method that subscribes on someone else’s behalf satisfies the rule by returning the handle — the caller then owns cancellation:
import 'dart:async';
StreamSubscription<String> watchMessages( Stream<String> messages, void Function(String) onMessage,) { return messages.listen(onMessage);}Collecting several subscriptions
Section titled “Collecting several subscriptions”Passing the handle as an argument counts too, which is the usual shape when one object owns a bag of subscriptions:
import 'dart:async';
class _FeedState extends State<Feed> { final Stream<String> _likes = const Stream<String>.empty(); final Stream<String> _comments = const Stream<String>.empty(); final _subscriptions = <StreamSubscription<String>>[];
@override void initState() { super.initState(); _subscriptions.add(_likes.listen(_onLike)); _subscriptions.add(_comments.listen(_onComment)); }
void _onLike(String id) {}
void _onComment(String id) {}
@override void dispose() { for (final subscription in _subscriptions) { subscription.cancel(); } super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}Known limitations
Section titled “Known limitations”The rule reports a listen(...) that stands alone as a statement. Anything that consumes the returned subscription — assigning it, returning it, passing it as an argument, awaiting it — satisfies the rule.
It checks only that the handle goes somewhere, not that it is ever cancelled. final subscription = stream.listen(...); with no matching cancel() passes this rule; the cancellation itself is dispose_fields’s job once the handle is a State field.
The receiver must resolve to something whose listen returns a dart:async StreamSubscription, so an unrelated listen method of your own is never flagged.
Turning this rule off
Section titled “Turning this rule off”This rule is in the core preset, so it is on with preset: core,
preset: recommended or preset: opinionated.
To turn it off:
rules: avoid_unassigned_stream_subscriptions: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Options
Section titled “Options”many_lints: rules: avoid_unassigned_stream_subscriptions: ignored_instances: [eventBus]rules: avoid_unassigned_stream_subscriptions: ignored_instances: [eventBus]| Option | Type | Default | Description |
|---|---|---|---|
ignored_instances |
list of strings | [] |
Receiver expressions whose subscriptions are torn down centrally |
Related rules
Section titled “Related rules”always_remove_listener— Ensure every addListener() has a matching removeListener() in dispose().avoid_late_final_reassignment— Flag alate finalfield assigned twice on one path.avoid_unremovable_callbacks_in_listeners— Don’t pass an inline closure to addListener.dispose_fields— Ensure State fields with disposal methods are cleaned up in dispose().