781flyingdutchman / background_downloader

Flutter plugin for file downloads and uploads
Other
158 stars 73 forks source link

Bad state: Stream has already been listened to. #133

Closed Hilbert2048 closed 1 year ago

Hilbert2048 commented 1 year ago

I am building a download app which has a screen to list all download tasks, user can enter this screen(page) and leave for multiple times.

1、listen to the stream in initState Method

@override
  void initState() {
    super.initState();
    subscription = FileDownloader().updates.listen((update) {
     // demo code 
      if (update is TaskStatusUpdate) {
        print('Status update for ${update.task} with status ${update.status}');
      } else if (update is TaskProgressUpdate) {
        print('Progress update for ${update.task} with progress ${update.progress}');
      }
    });
  }

  @override
  void dispose() {
    subscription.cancel();
    super.dispose();
  }

2、when enter the page the second time, this will throw the error “Bad state: Stream has already been listened to”. Now even if you cancel the first subscription and subscribe again you’ll still get this error and that is by design.

======== Exception caught by widgets library =======================================================
The following StateError was thrown building KeepAliveWrapper(state: _KeepAliveWrapperState#6bddd):
Bad state: Stream has already been listened to.

https://medium.com/flutter-community/flutter-stream-basics-for-beginners-eda23e44e32f

Hilbert2048 commented 1 year ago

@781flyingdutchman I submit a pull request to fix this, pls review the code to see if there is any problem.

781flyingdutchman commented 1 year ago

You should simply call FileDownloader().reset Updates() before attempting to listen to the updates stream again. A broadcast stream is not the right type of stream for these updates

781flyingdutchman commented 1 year ago

Also, if I understand your description correctly, I believe your approach is incorrect. Unless you are sure your user won't leave the downloads page you're describing, you'll miss status updates that will lead to incorrect state being displayed. Generally, you should have a listener active at all times, and use those to maintain global state - not page/screen state. Your download page can then simply listen to that global state and render it on your screen.

My suggestion is to create a BLOC or singleton object that acts as the intermediary to the FileDownloader. It starts the downloads and always listens to updates so it can maintain state of all downloaded files. Your screen then renders that state, and rerenders when state changes (this is pretty standard BLOC architecture).

Hope this helps.

praveengitsit commented 1 year ago

@781flyingdutchman

Calling "FileDownloader().resetUpdates()" before listening to the updates stream again does not fix the issue.

praveengitsit commented 1 year ago

@781flyingdutchman

I also tried this with the BLoC architecture. Created a bloc for background downloader (lets call it BackgroundDownloaderBloc), and a (bloc) repository provider for FileDownloader, above the MaterialApp so side-effects for page states does not affect the package implementation.

Then, I opened a subscription for fileDownloader.updates.listen which emits bloc states for task status updates of type TaskStatus.

If you close the app, and open it again, the same set of issues as present before when navigating back and forth arrives.

781flyingdutchman commented 1 year ago

It still sounds like you're starting to listen at a point that is reentered multiple times. You need to listen once, in a singleton that does not get recreated.

Blacktaler commented 11 months ago

I think there should be another solution for this. Because there might be a situation like, we need to redirect to the first Widget in the tree. For example, sometimes we need Navigator.pushAndRemoveUntil function in order to redirect the user to the first page. Then we should be able to listen for the updates. But it gives error. Fix this please

781flyingdutchman commented 11 months ago

Whenever your app is running, you should be listening to updates, unless you are 100% certain that there is no task running. Therefore, you should have your listener in a singleton-like object that lives throughout the life of your app, and where you maintain state. The fact that you may require to dopushAndRemoveUntil doesn't change anything: if your listener is attached to a widget deep in your tree, that isn't always alive, then you should probably rethink your architecture.

Per above, you can use FileDownloader().resetUpdates() to reset the stream before you listen, but again, you're setting yourself up for inconsistent state issues.

Blacktaler commented 11 months ago

Sorry, I didn't understand but now I know that it works perfectly If I use it correctly. I thought if I need to update the state while listening, but outside material widget it wouldn't be initialized. However in order to start any download, the ui should be fully drawn. So now I got it, thanks :)