kranfix / riverbloc

`flutter_bloc` implemented with `riverpod` instead of `provider`.
83 stars 17 forks source link

[Proposal] Unsubscribe from the stream if you are not using it #75

Closed BreX900 closed 2 years ago

BreX900 commented 2 years ago

Listening to the bloc stream for BlocProvider happens forever once listening has started. In my opinion the only resource that should live forever is the bloc. On the other hand, when the widget that is listening to the stream is no longer in the UI, this stream should no longer be listened to

You can verify this problem with: (Bloc/Cubit)

  @override
  late final Stream<ValueState<T>> stream = super.stream.doOnCancel(() {
    print('Cancel');
  }).doOnListen(() {
    print('Listen');
  });

Cancel will never be called as listening to the stream always remains alive

The correct implementation would be to update BlocProvider with:

  @override
  AutoDisposeProviderElementBase<S> createElement() {
    return AutoDisposeProviderElement(this);
  }

I hope I have explained, the behavior does not want to be that of BlocProvider.autoDispose but only free them to subscribe to the stream, not the entire bloc

kranfix commented 2 years ago

Can you provide a gist to reproduce the issue?

On the other hand, If a widget is removed from the widget tree, you must cancel the stream suscription manually in the dispose method.

If you want to subscribe to the state, you can use ref.listen

final counterProvider = BlocProvider.autodispose<CounterBloc, int>((ref) => CounterBloc(0));  
ref.listen(  
  counterProvider,  
  (int? previous, int current) {},  
);  

This must call the cancel when the widget is removed from the widget tree.

But, if you use BlocProvider instead of AutoDisposeBlocProvider, the bloc always be alive, and then, the stream will always alive.

The only way to kill the a bloc and its stream in a BlocProvider  is to remover the ProviderScope or using ref.refresh(counterProvider).

If you want to learn how to apply the first idea, maybe you want to study this [repository](https://github.com/kranfix/river_navigator)  where I explored how to work with nested navigators with multiple ProviderScope.

It is not possible to cancel the stream of the bloc in a BlocProvider because if it dies, it can never be alive again.

BreX900 commented 2 years ago

@kranfix Here is an example: https://gist.github.com/BreX900/f6c67db6a907829010110b726e88a01f FixedBlocProvider would be the correct implementation of BlocProvider in my opinion

If you tap and close the dialog you will see this log series:

I/flutter (21829): Create: Normal        // First Dialog open 
I/flutter (21829): Listen: Normal
I/flutter (21829): Create: Fixed          // First Dialog open
I/flutter (21829): Listen: Fixed
I/flutter (21829): Cancel: Fixed        // Dialog closed
I/flutter (21829): Listen: Fixed        // Dialog reopen
I/flutter (21829): Cancel: Fixed        // Dialog recessed

As you can see, you could stream the bloc with auto dispose active so as to stop listening to the stream if it is no longer necessary. This implementation keeps the block alive, so it will restart listening to the block stream if you reopen the dialog, so if you listen again. Instead the current implementation continues to listen to the block stream even if it is not needed

Simply put, the stream provider should always be an autoDispose and the bloc provider should be the one defined by the developer

kranfix commented 2 years ago

I was analyzing your example and I have a question. Instead of using BlocProvider, why don't you use AutoDisposeBlocProvider or its equivalente BlocProvider.autodispose?

The AutoDisposeBlocProvider must work as your FixedBlocProvider.

When your dialog is removed from the widget-tree, the bloc.stream is not disposed only the current subscription.

SimoneBressan commented 2 years ago

@kranfix What you get from my example is:

What you would get with AutoDisposeBlocProvider is:

I would like only the subscription to die and not the bloc


I saw that in riverpod in dev version this was added:

2.0.0-dev.2: Added ref.onAddListener, ref.onRemoveListener, ref.onCancel and ref.onResume. All of which allow performing side-effects when providers are listened or stop being listened.

The effect you get with my example is the same as the one you would get with this code in the riverpod version mentioned:

  @override
  S create(ProviderElementBase<S> ref) {
    final bloc = ref.watch(this.bloc);

    void listener(S newState) => ref.setState(newState);

    int listenersCount = 0;
    StreamSubscription? subscription;

    ref.onAddListener(() {
      listenersCount += 1;
      if (listenersCount > 0 && subscription == null) {
        subscription = bloc.stream.listen(listener);
      }
    });

    ref.onRemoveListener(() {
      listenersCount -= 1;
      if (listenersCount < 1 && subscription != null) {
        subscription?.cancel();
        subscription = null;
      }
    });

    return bloc.state;
  }

Haven't tried but the right behavior I would expect would be this :)

kranfix commented 2 years ago

The AutoDisposeBlocProvider will close the bloc and its stream when it is not listened/watched anymore.

kranfix commented 2 years ago

The idea is to avoid stream subscriptions