Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stream from StreamProvider is not unsubscribed to when widgets get disposed #3562

Open
tobiiasl opened this issue May 21, 2024 · 3 comments
Open
Assignees
Labels
bug Something isn't working question Further information is requested

Comments

@tobiiasl
Copy link

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 tobiiasl added bug Something isn't working needs triage labels May 21, 2024
@smurat
Copy link

smurat commented Jun 10, 2024

I'm having a similar problem

@rrousselGit
Copy link
Owner

Theres nothing wrong here. Your provider wasn't disposed. So the Stream is still active.
Use autoDispose.

@rrousselGit rrousselGit added question Further information is requested and removed needs triage labels Jun 10, 2024
@tobiiasl
Copy link
Author

tobiiasl commented Jun 12, 2024

Perhaps the test setup is just confusing. I will instead provide code closer to the real use case below.

Our app has a provider which exposes a stream from an event channel, basically just return eventChannel.receiveBroadcastStream(). On the Java side, we start using HW resources when the event channel is listened to (in the Java onListen() method) and stop using them when the event channel no longer has any listeners (in the Java onCancel() method). The problem is that the event channel stream never gets unsubscribed from and therefore the Java onCancel() method never gets executed.

In the code below I have replaced the EventChannel object with a simple StreamController().stream object to keep things simple and in a single file. But think of it as an event channel.

When toggling between the two views I expect to see both Event: onListen and Event: onCancel being printed. But only Event: onListen gets printed once.

The goal is to make the onCancel() method on the Java side being called when no widget is listening to the event channel stream. How can that be achieved?

import 'dart:async';

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

// This is simplified code (to not have to implement Java code).
final eventChannelStreamController = StreamController<int>();
final eventChannelStream = eventChannelStreamController.stream;

// More similar to real code:
// const eventChannel = EventChannel('event_channel');
// final eventChannelStream = eventChannel.receiveBroadcastStream();

final streamProvider = StreamProvider.autoDispose<int>((ref) {
  ref.onDispose(() {
    // Should something be done here?
  });

  return eventChannelStream.asBroadcastStream();
});

void main() {
  eventChannelStreamController.onListen = () {
    print('Event: onListen');
  };

  eventChannelStreamController.onCancel = () {
    print('Event: onCancel');
  };

  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  bool showTestWidget = true;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body:
            showTestWidget ? const TestWidget() : const Text('Another widget'),
        floatingActionButton: FloatingActionButton(
          onPressed: () => setState(() => showTestWidget = !showTestWidget),
          child: Icon(showTestWidget ? Icons.visibility_off : Icons.visibility),
        ),
      ),
    );
  }
}

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants