riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

Stream from StreamProvider is not unsubscribed to when widgets get disposed

Open tobiiasl opened this issue 1 month ago • 3 comments

Describe the bug When a widget that watches a stream provider gets disposed, it should unsubscribe to the stream returned by the stream provider. This is not the case.

Real problem: I need to dispose of some resources related to a stream controller when there are no longer any listeners to its stream. But even though the widget watching the stream provider has been disposed, the onCancel method on the stream controller does not get called. This prevents me from disposing the resources.

To Reproduce

Here is a test showing that even though the TestWidget is disposed, the stream returned from the stream provider still has listeners and that the onCancel method does not get called.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../utils/riverpod_test_utils.dart';

final realStreamProvider = StreamProvider<int>((ref) => const Stream.empty());

class TestWidget extends ConsumerWidget {
  const TestWidget({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ref.watch(realStreamProvider).when(
        data: (value) => Container(),
        loading: () => Container(),
        error: (error, stack) => Container());
  }
}

void main() {
  testWidgets('verify widget unsubscribes to real stream', (tester) async {
    final controller = StreamController<int>(sync: true);
    bool onCancelHasBeenCalled = false;

    controller.onCancel = () {
      onCancelHasBeenCalled = true;
    };

    await tester.pumpRiverpodWidget(const TestWidget(),
        override: realStreamProvider.overrideWith((ref) => controller.stream));
    await tester.pumpAndSettle();

    expect(controller.hasListener, true);

    await tester.pumpWidget(const SizedBox());
    await tester.pumpAndSettle();

    expect(false, controller.hasListener); // FAILS
    expect(true, onCancelHasBeenCalled); // FAILS

    await controller.close();
  });
}

Expected behavior

There should be no listeners to the stream after the widget has been disposed.

tobiiasl avatar May 21 '24 09:05 tobiiasl