49

I am trying to use the bloc pattern to manage data from an API and show them in my widget. I am able to fetch data from API and process it and show it, but I am using a bottom navigation bar and when I change tab and go to my previous tab, it returns this error:

Unhandled Exception: Bad state: Cannot add new events after calling close.

I know it is because I am closing the stream and then trying to add to it, but I do not know how to fix it because not disposing the publishsubject will result in memory leak. here is my Ui code:

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  @override
  void initState() {
    serviceBloc.getAllServices();
    super.initState();
  }

  @override
  void dispose() {
    serviceBloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: serviceBloc.allServices,
      builder: (context, AsyncSnapshot<ServiceModel> snapshot) {
        if (snapshot.hasData) {
          return _homeBody(context, snapshot);
        }
        if (snapshot.hasError) {
          return Center(
            child: Text('Failed to load data'),
          );
        }
        return CircularProgressIndicator();
      },
    );
  }
}

_homeBody(BuildContext context, AsyncSnapshot<ServiceModel> snapshot) {
  return Stack(
      Padding(
          padding: EdgeInsets.only(top: screenAwareSize(400, context)),
          child: _buildCategories(context, snapshot))
    ],
  );
}

_buildCategories(BuildContext context, AsyncSnapshot<ServiceModel> snapshot) {
  return Padding(
    padding: EdgeInsets.symmetric(vertical: 20),
    child: GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3, crossAxisSpacing: 3.0),
      itemCount: snapshot.data.result.length,
      itemBuilder: (BuildContext context, int index) {
        return InkWell(
          child: CategoryWidget(
            title: snapshot.data.result[index].name,
            icon: Icons.phone_iphone,
          ),
          onTap: () {},
        );
      },
    ),
  );
}

here is my bloc code:

class ServiceBloc extends MainBloc {
  final _repo = new Repo();
  final PublishSubject<ServiceModel> _serviceController =
      new PublishSubject<ServiceModel>();
  Observable<ServiceModel> get allServices => _serviceController.stream;
  getAllServices() async {
    appIsLoading();
    ServiceModel movieItem = await _repo.getAllServices();
    _serviceController.sink.add(movieItem);
    appIsNotLoading();
  }

  void dispose() {
    _serviceController.close();
  }
}

ServiceBloc serviceBloc = new ServiceBloc();

I did not include the repo and API code because it is not in the subject of this error.

geekymano
  • 1,420
  • 5
  • 22
  • 53

10 Answers10

38

Use StreamController.isClosed to check if the controller is closed or not, if not closed add data to it.

if (!_controller.isClosed) 
  _controller.sink.add(...); // safe to add data as _controller isn't closed yet

From Docs:

Whether the stream controller is closed for adding more events.

The controller becomes closed by calling the close method. New events cannot be added, by calling add or addError, to a closed controller.

If the controller is closed, the "done" event might not have been delivered yet, but it has been scheduled, and it is too late to add more events.

Community
  • 1
  • 1
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
22

If the error is actually caused by the code you posted, I'd just add a check to ensure no new events are added after dispose() was called.

class ServiceBloc extends MainBloc {
  final _repo = new Repo();
  final PublishSubject<ServiceModel> _serviceController =
      new PublishSubject<ServiceModel>();
  Observable<ServiceModel> get allServices => _serviceController.stream;
  getAllServices() async {
    // do nothing if already disposed
    if(_isDisposed) {
      return;
    }
    appIsLoading();
    ServiceModel movieItem = await _repo.getAllServices();
    _serviceController.sink.add(movieItem);
    appIsNotLoading();
  }

  bool _isDisposed = false;
  void dispose() {
    _serviceController.close();
    _isDisposed = true;
  }
}

ServiceBloc serviceBloc = new ServiceBloc();
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
4

You should not worry about memory leak while using flutter_bloc as When using bloc you do not need to close the bloc manually, if you have used a bloc provider to inject the bloc. Bloc Providers handle that for you out of the box as mentioned in the flutter_bloc docs.

BlocProvider is responsible for creating the bloc, it will automatically handle closing the bloc

You can test this in your application. Try printing on the close() override of bloc.

If the Screen at which the bloc was provided is removed from navigation stack then the close() method for that given bloc is called out of the box.

Ritesh Singh
  • 782
  • 9
  • 19
3

I run into same error and noticed that if you check isClosed, the screen is not updated. In your code you have to remove the last line from Bloc file:

ServiceBloc serviceBloc = new ServiceBloc();

and put this line in CategoryPage just before the initState(). This way your widget is creating and disposing the bloc. Before, the widget only disposes the bloc but it is never re-created when the widget is re-created.

cwhisperer
  • 1,478
  • 1
  • 32
  • 63
3

besides the provided solution I think you should also drain the stream allServices used in your ServiceBloc with:

@override
void dispose() {
      ...
      allServices?.drain(); 
}
Julian2611
  • 422
  • 3
  • 11
3

Check if the bloc/cubit is closed by isClosed variable. Wrap this if conditions to those states which are throwing exception.

Example code

class LandingCubit extends Cubit<LandingState> {
  LandingCubit(this.repository) : super(LandingInitial());
  final CoreRepository repository;

  // Fetches image urls that needs to shown in landing page
  void getLandingImages() async {
    emit(LandingImagesLoading());
    try {
      List<File> landingImages = await repository.landingImages();
      if (!isClosed) {
        emit(LandingImagesSuccess(landingImages));
      }
    } catch (e) {
      if (!isClosed) {
        emit(LandingImagesFetchError(e.toString()));
      }
    }
  }
}
BLB
  • 663
  • 1
  • 14
  • 27
2

@cwhisperer is absolutely right. Initialize and dispose your block inside widget just like bellow.

final ServiceBloc serviceBloc = new ServiceBloc();

  @override
  void initState() {
    serviceBloc.getAllServices();
    super.initState();
  }

  @override
  void dispose() {
    serviceBloc.dispose();
    super.dispose();
  }

and delete ServiceBloc serviceBloc = new ServiceBloc(); from your class ServiceBloc

1
ServiceBloc serviceBloc = new ServiceBloc();

// remove this code // don't init class in the same page that will cause of bad state.

TuGordoBello
  • 4,350
  • 9
  • 52
  • 78
reimi
  • 179
  • 2
  • 4
1

I also faced this issue in production, and I realized that we should either dispose BehaviorSubject (or any other StreamController) when the Widget is disposed or Check to see if Stream is closed before adding new value.

Here is a nice extension to do all the job:

extension BehaviorSubjectExtensions <T> on BehaviorSubject<T> {
  set safeValue(T newValue) => isClosed == false ? add(newValue) : () {};
}

You can use it like so:

class MyBloc {
  final _data = BehaviorSubject<String>();
  
  void fetchData() {
    // get your data from wherever it is located
    _data.safeValue = 'Safe to add data';
  }

  void dispose() {
    _data.close();
  }
}

How to dispose in Widget:

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  late MyBloc bloc;

  @override
  void initState() {
    bloc = MyBloc();
    bloc.fetchData();
    super.initState();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  // Other part of your Widget
}
Mahdi Javaheri
  • 1,080
  • 13
  • 25
0

even better, if you aren't sure you won't reuse the stream after disposing:

call the drain() function on the stream before closing the stream.

dispose() async{
await _coinDataFetcher.drain();
_coinDataFetcher.close();
_isdisposed = true;

}