felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.8k stars 3.39k forks source link

Bad state: Cannot add new events after calling close after the bloc is disposed. #120

Closed haqqi closed 5 years ago

haqqi commented 5 years ago

I got this error while trying a scenario:

  1. I need to fetch some data from the API.
  2. To simulate the fetching delay time, i use await Future.delayed(Duration(seconds: 2)); in the mapEventToState bloc. Then i yield some state.
  3. I dispose the bloc when the page is popped.
  4. When i press back (dispose) the page before the fetching finished (under 2 seconds), the error below happened. The apps did not crashed though.

Seems like it is similar with #52

E/flutter (14028): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: Bad state: Cannot add new events after calling close
E/flutter (14028): #0      _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:249:24)
E/flutter (14028): #1      Subject._add (package:rxdart/src/subjects/subject.dart:124:16)
E/flutter (14028): #2      Subject.add (package:rxdart/src/subjects/subject.dart:118:5)
E/flutter (14028): #3      Bloc._bindStateSubject.<anonymous closure> (package:bloc/src/bloc.dart:86:23)
E/flutter (14028): #4      Stream.forEach.<anonymous closure>.<anonymous closure> (dart:async/stream.dart:814:45)
E/flutter (14028): #5      _runUserCode (dart:async/stream_pipe.dart:11:23)
E/flutter (14028): #6      Stream.forEach.<anonymous closure> (dart:async/stream.dart:814:11)
E/flutter (14028): #7      _rootRunUnary (dart:async/zone.dart:1132:38)
E/flutter (14028): #8      _CustomZone.runUnary (dart:async/zone.dart:1029:19)
E/flutter (14028): #9      _CustomZone.runUnaryGuarded (dart:async/zone.dart:931:7)
E/flutter (14028): #10     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:336:11)
E/flutter (14028): #11     _BufferingStreamSubscription._add (dart:async/stream_impl.dart:263:7)
E/flutter (14028): #12     _SyncBroadcastStreamController._sendData (dart:async/broadcast_stream_controller.dart:375:20)
E/flutter (14028): #13     _BroadcastStreamController._add (dart:async/broadcast_stream_controller.dart:287:5)
E/flutter (14028): #14     _rootRunUnary (dart:async/zone.dart:1132:38)
E/flutter (14028): #15     _CustomZone.runUnary (dart:async/zone.dart:1029:19)
E/flutter (14028): #16     _CustomZone.runUnaryGuarded (dart:async/zone.dart:931:7)
E/flutter (14028): #17     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:336:11)
E/flutter (14028): #18     _BufferingStreamSubscription._add (dart:async/stream_impl.dart:263:7)
E/flutter (14028): #19     _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:132:11)
E/flutter (14028): #20     _ForwardingStream._handleData (dart:async/stream_pipe.dart:98:10)
E/flutter (14028): #21     _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:164:13)
E/flutter (14028): #22     _rootRunUnary (dart:async/zone.dart:1132:38)
E/flutter (14028): #23     _CustomZone.runUnary (dart:async/zone.dart:1029:19)
E/flutter (14028): #24     _CustomZone.runUnaryGuarded (dart:async/zone.dart:931:7)
E/flutter (14028): #25     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:336:11)
E/flutter (14028): #26     _DelayedData.perform (dart:async/stream_impl.dart:591:14)
E/flutter (14028): #27     _StreamImplEvents.handleNext (dart:async/stream_impl.dart:707:11)
E/flutter (14028): #28     _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:667:7)
E/flutter (14028): #29     _rootRun (dart:async/zone.dart:1120:38)
E/flutter (14028): #30     _CustomZone.run (dart:async/zone.dart:1021:19)
E/flutter (14028): #31     _CustomZone.runGuarded (dart:async/zone.dart:923:7)
E/flutter (14028): #32     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:963:23)
E/flutter (14028): #33     _rootRun (dart:async/zone.dart:1124:13)
E/flutter (14028): #34     _CustomZone.run (dart:async/zone.dart:1021:19)
E/flutter (14028): #35     _CustomZone.runGuarded (dart:async/zone.dart:923:7)
E/flutter (14028): #36     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:963:23)
E/flutter (14028): #37     _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
E/flutter (14028): #38     _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)
E/flutter (14028): 
felangel commented 5 years ago

@haqqi thanks for opening an issue!

I would recommend creating and disposing the bloc one level above where you are doing it now to resolve this. The bloc should be disposed only when you are certain it is no longer being used.

Let me know if that helps! 👍

felangel commented 5 years ago

Closing this for now. Feel free to comment if that didn’t help and I will reopen the issue.

haqqi commented 5 years ago

@haqqi thanks for opening an issue!

I would recommend creating and disposing the bloc one level above where you are doing it now to resolve this. The bloc should be disposed only when you are certain it is no longer being used.

Let me know if that helps! 👍

Actually i use the bloc at highest level, which is just below body tag of Scaffold. I could not put the bloc in the MaterialApp, because the bloc is supposed to be used in just one page/route only.

felangel commented 5 years ago

@haqqi can you please share a sample application which illustrates the problem you're having?

glourencosilva commented 5 years ago

Hello, Felangel, how are you? Suggest that in a next version, make an adjustment in the class Bolc. In the dispose method, I did this way and the error stopped being displayed.

  /// Closes the [Event] and [State] [Stream]s.
  @mustCallSuper
  void dispose() async{
    await _eventSubject.drain();
    _eventSubject.close();
    await _stateSubject.drain();
    _stateSubject.close();
  }
felangel commented 5 years ago

@glourencosilva thanks for the suggestion but I don't think we want to hide the error. The error is valuable because it indicates to the developer that they disposed the bloc while it is still being used in other parts of the application.

basketball-ico commented 5 years ago

@felangel I like to see this error in console, because of this I know that streamSubscription that I created in the bloc are trying to dispatch something, and I can fix that behavior, without that i never know that this subscription are not closed; thanks

felangel commented 5 years ago

@basketball-ico you will see this error in the console if you override onError and print errors to the console.

You can either override onError at the bloc level (for a specific bloc) or globally in BlocDelegate.

Hope that helps 👍

jifferon commented 4 years ago

Hello, Felangel, how are you? Suggest that in a next version, make an adjustment in the class Bolc. In the dispose method, I did this way and the error stopped being displayed.

  /// Closes the [Event] and [State] [Stream]s.
  @mustCallSuper
  void dispose() async{
    await _eventSubject.drain();
    _eventSubject.close();
    await _stateSubject.drain();
    _stateSubject.close();
  }

Holly molly! That totally saved my ass after 2 days of research! I'm not even using this library, just plain small Bloc class which has one dispose() method and list of subscriptions. It was working just fine, but then I've noticed that sometimes web version would crash randomly. After this fix it works like charm, thank you very much!

jacinto-joao commented 4 years ago

Hi @felangel, I'm having similar issuer,Cannot add new events after calling close

What's the best way to avoid this error.

In my case, I have 3 screens Maps View, List View and Filters

Inside maps view is where I dispatch the event and listen to events changes in other screens.

When landing on the maps view, I dispatch the even, to make a call to the API to get data.

The same data I populate for both Maps and List view.

and the filter screen I just dispatch the event to filter the data, and the same filtered data should be equal in the maps and list view.

But the issue I'm facing, when I make a filter from the Maps View, and go to List View, try to filter again, it says Cannot add new events after calling close, which means I only can dispatch the filter event from one screen.

Can you please help to understand nicely how to avoid this kind of error

felangel commented 4 years ago

Hi @jjoao07 make sure you aren't manually disposing/closing blocs (leave it to be handled by BlocProvider instead) and you should be good to go.

sivaram16 commented 4 years ago

Hi @felangel I am using flutter_bloc: ^3.1.0, I hope in latest version of the bloc we don't need to dispose the bloc and in my project i am not disposing any of my bloc, so I am having three tab bars each one has seperate bloc whenever i am opening new tabs doing some operations then back to another tab when i click something getting error. " Bad state: Cannot add new events after calling close"

thanks in advance

felangel commented 4 years ago

@ThalapathySiva can you please provide a link to a sample app which illustrates the problem you're having? You shouldn't need to manually dispose your blocs but the error you're getting means that somewhere in your app something is trying to add events to the bloc after the bloc has been disposed. Usually, the solution is to raise your BlocProvider higher in the widget tree.

BenevidesLecontes commented 4 years ago

@ThalapathySiva i'm having the same issue, with the same scenario.

bintangursiboro commented 4 years ago

Hello @felangel , just upgrade bloc version to 4.0.0 (3.0.0 before), and then randomly get error:

I/flutter (25994): Another exception was thrown: Unhandled error Bad state: Cannot add new events after calling close occurred in bloc Instance of 'ArticleYoutubeBloc'. E/flutter (25994): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: 'package:flutter/src/widgets/scroll_activity.dart': Failed assertion: line 599 pos 15: 'to != null': is not true. E/flutter (25994): #0 _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:42:39) E/flutter (25994): #1 _AssertionError._throwNew (dart:core-patch/errors_patch.dart:38:5) E/flutter (25994): #2 new DrivenScrollActivity (package:flutter/src/widgets/scroll_activity.dart:599:15) E/flutter (25994): #3 ScrollPositionWithSingleContext.animateTo (package:flutter/src/widgets/scroll_position_with_single_context.dart:183:43) E/flutter (25994): #4 ScrollController.animateTo (package:flutter/src/widgets/scroll_controller.dart:153:37) E/flutter (25994): #5 _MarqueeWidgetState.scroll (package:finoo/ui/marquee_widget.dart:49:28) E/flutter (25994): <asynchronous suspension> E/flutter (25994): #6 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1102:15) E/flutter (25994): #7 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1049:9) E/flutter (25994): #8 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:957:5) E/flutter (25994): #9 _rootRun (dart:async/zone.dart:1126:13) E/flutter (25994): #10 _CustomZone.run (dart:async/zone.dart:1023:19) E/flutter (25994): #11 _CustomZone.runGuarded (dart:async/zone.dart:925:7) E/flutter (25994): #12 _invoke (dart:ui/hooks.dart:259:10) E/flutter (25994): #13 _drawFrame (dart:ui/hooks.dart:217:3)

and after this error, the red error box will take over the app.

felangel commented 4 years ago

Hi @bintangursiboro 👋 Are you able to share a link to a sample which reproduces the issue so I can run/debug it locally? Thanks 👍

bintangursiboro commented 4 years ago

Currently cannot share any link for you, but here's a thing, So I have bottom navigation -> Home, Settings, etc. so I use bloc provider for temp saving the home state (fetching data from server at the first time when app launch), and then after I tap setting navigation, and back tap home, and bloc is re fetching data from server again. In previous version, bloc is fetching data just once when launching the app. Hope this detail is helping. Thank you

satishsoni777 commented 4 years ago

Can anyone tell me, how to cancel the previous event and add a new event?.

felangel commented 4 years ago

@satishsoni777 have you tried overriding transformEvents to use switchMap?

satishsoni777 commented 4 years ago

No, I have not tried. could you please share more details about it.?

narcodico commented 4 years ago

Hi @satishsoni777 👋

Please have a look at https://github.com/felangel/bloc/issues/1107#issuecomment-621176290 for an example. 👍

slimshady060 commented 4 years ago

@glourencosilva is the solution... TKS

FelipeCabeza16 commented 4 years ago

@felangel Hi Dear Felix, i've using your library since a long time ago, but i've a question, for example i use a bloc to register new users and if i call _registerBloc.close() on dispose(), i can't register more users, if the user calls again this screen because close events added will not processed, and i've the same trouble with another blocs, the user maybe don't use immediately but later can be use a bloc again, if i not call dispose(), the app work well, but it is a memory fail? what's happens if I never call close() on the blocs? Maybe i've 8 on my app.

Thank for your help, nice day.

SivaGiga commented 3 years ago

Im getting same issue, Im using audioplayers and google_mobile_ads sdk.. [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Bad state: Cannot add new events after calling close E/flutter (17325): #0 _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:243:24) E/flutter (17325): #1 AudioPlayer.state= (package:audioplayers/src/audioplayer.dart:70:28) E/flutter (17325): #2 AudioPlayer.pause (package:audioplayers/src/audioplayer.dart:240:7) E/flutter (17325): How to fix this

felangel commented 3 years ago

@SivaGiga based on the stacktrace it looks like this is an issue with your AudioPlayer and doesn't appear to have anything to do with bloc. My guess is you're disposing the audio player and then later trying to interact with it.

ZillionCAdmin commented 2 years ago

@felangel , I have a button captioned "Search Location". On Click this button i'm opening a showModalBottomSheet (DraggableScrrollaleSheet). This bottom sheet has a list of location and a search bar to search the location.

When i open this sheet for first time, It properly search for the location and i select it and close the bottom sheet. Now when second time open this sheet and search for the location it throws error.

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

Please help.

narcodico commented 2 years ago

@ZillionCAdmin it means your bloc was disposed, thus you cannot add events to it anymore. Make sure you're working with a valid instance.

ZillionCAdmin commented 2 years ago

@narcodico , I think i need to share my code. As this is my learning content, i think i can share with you.

class RegistrationStep extends StatefulWidget {
  const RegistrationStep({Key? key}) : super(key: key);

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

enum Gender { male, female }

class _RegistrationStepState extends State<RegistrationStep> {
  late Gender? gender = Gender.male;
  // final _scrollController = ScrollController();
  final RegistrationBloc registrationBloc =
      RegistrationBloc(locationSearchRepository: LocationSearchRepository());
  // final _scrollThreashold = 200.0;

  _RegistrationStepState() {
    registrationBloc.add(LocationOnLoadFetchData());
    // _scrollController.addListener(_onScroll);
  }

  final Debounce _debounce = Debounce(const Duration(milliseconds: 500));

  final List<StudentClassDegree> studentClassDegreeList = [
    StudentClassDegree(educationTitle: "Class V", qualificationId: 1),
    StudentClassDegree(educationTitle: "Class VI", qualificationId: 2),
    StudentClassDegree(educationTitle: "Class VII", qualificationId: 3),
    StudentClassDegree(educationTitle: "Class VIII", qualificationId: 4),
    StudentClassDegree(educationTitle: "Class IX", qualificationId: 5),
    StudentClassDegree(educationTitle: "Class X", qualificationId: 6),
    StudentClassDegree(educationTitle: "Class XI", qualificationId: 7),
    StudentClassDegree(educationTitle: "Class XII", qualificationId: 8),
    StudentClassDegree(educationTitle: "B.Com.", qualificationId: 9),
    StudentClassDegree(educationTitle: "B.Tech", qualificationId: 10),
    StudentClassDegree(educationTitle: "BCA", qualificationId: 11),
    StudentClassDegree(educationTitle: "MCom", qualificationId: 12),
    StudentClassDegree(educationTitle: "MCA", qualificationId: 13),
    StudentClassDegree(educationTitle: "B.Ed", qualificationId: 14),
    StudentClassDegree(educationTitle: "Diploma", qualificationId: 15),
    StudentClassDegree(educationTitle: "M.Tech", qualificationId: 16),
    StudentClassDegree(educationTitle: "MBA", qualificationId: 17),
    StudentClassDegree(educationTitle: "BBA", qualificationId: 18),
    StudentClassDegree(educationTitle: "LLB", qualificationId: 19),
    StudentClassDegree(educationTitle: "Commerce", qualificationId: 20),
    StudentClassDegree(educationTitle: "BCom. Honors", qualificationId: 21),
    StudentClassDegree(educationTitle: "BTC", qualificationId: 22),
    StudentClassDegree(educationTitle: "CTET", qualificationId: 23),
    StudentClassDegree(educationTitle: "TET", qualificationId: 24)
  ];

  int selectedCityIndex = -1;
  String selectedCityText = 'Search from location';

  @override
  void dispose() {
    registrationBloc.close();
    _debounce.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey.shade100,
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        backgroundColor: Colors.cyan,
        title: const Text("Setup your profile"),
        actions: [
          IconButton(onPressed: () {}, icon: const Icon(Icons.logout_rounded))
        ],
      ),
      body: SingleChildScrollView(
          child: Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Padding(
                      padding: EdgeInsets.only(top: 10, left: 8),
                      child: Text(
                        "Basic Detail",
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ),
                    _nameEmailCardView(),
                    const SizedBox(
                      height: 10,
                    ),
                    const Padding(
                      padding: EdgeInsets.only(top: 10, left: 8),
                      child: Text(
                        "Gender and Your Birthdate",
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ),
                    _ageAndGenderCardView(),
                    const SizedBox(
                      height: 10,
                    ),
                    const Padding(
                      padding: EdgeInsets.only(top: 10, left: 8),
                      child: Text(
                        "Qualification and School",
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ),
                    _standardAndSchoolCollegeCardView(),
                    const SizedBox(
                      height: 10,
                    ),
                    const Padding(
                      padding: EdgeInsets.only(top: 10, left: 8),
                      child: Text(
                        "Address Detail",
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ),
                    _addressCardView(),
                  ]))),
    );
  }

  Widget _nameEmailCardView() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(15.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Padding(
              padding: EdgeInsets.only(bottom: 5),
              child: Text("What's your name"),
            ),
            TextFormField(
              style: const TextStyle(fontWeight: FontWeight.normal),
              decoration: InputDecoration(
                  contentPadding: const EdgeInsets.symmetric(horizontal: 10),
                  hintText: "Full Name (required)",
                  enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                      borderSide: const BorderSide(color: Colors.black12)),
                  focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                      borderSide: const BorderSide(color: Colors.cyan))),
            ),
            const SizedBox(height: 20),
            const Padding(
              padding: EdgeInsets.only(bottom: 5),
              child: Text("Email"),
            ),
            TextFormField(
              decoration: InputDecoration(
                  contentPadding: const EdgeInsets.symmetric(horizontal: 10),
                  hintText: "Email (optional)",
                  enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                      borderSide: const BorderSide(color: Colors.black12)),
                  focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                      borderSide: const BorderSide(color: Colors.cyan))),
            )
          ],
        ),
      ),
    );
  }

  Widget _standardAndSchoolCollegeCardView() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(15.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Padding(
              padding: EdgeInsets.only(bottom: 5),
              child: Text("What's your name"),
            ),
            TextFormField(
              style: const TextStyle(fontWeight: FontWeight.normal),
              decoration: InputDecoration(
                  contentPadding: const EdgeInsets.symmetric(horizontal: 10),
                  hintText: "Full Name (required)",
                  enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                      borderSide: const BorderSide(color: Colors.black12)),
                  focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                      borderSide: const BorderSide(color: Colors.cyan))),
            ),
            const SizedBox(height: 20),
            const Padding(
              padding: EdgeInsets.only(bottom: 5),
              child: Text("Email"),
            ),
            TextFormField(
              decoration: InputDecoration(
                  contentPadding: const EdgeInsets.symmetric(horizontal: 10),
                  hintText: "Email (optional)",
                  enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                      borderSide: const BorderSide(color: Colors.black12)),
                  focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                      borderSide: const BorderSide(color: Colors.cyan))),
            )
          ],
        ),
      ),
    );
  }

  Widget _ageAndGenderCardView() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(15.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Padding(
              padding: EdgeInsets.only(bottom: 5),
              child: Text("Gender"),
            ),
            Row(
              children: [
                Container(
                  width: 160,
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(4),
                      border: Border.all(color: Colors.grey.shade500)),
                  child: RadioListTile(
                      contentPadding: const EdgeInsets.all(0),
                      title: const Text("Male"),
                      value: Gender.male,
                      groupValue: gender,
                      onChanged: (Gender? value) {
                        setState(() {
                          gender = value;
                        });
                      }),
                ),
                const SizedBox(
                  width: 10,
                ),
                Container(
                    width: 160,
                    decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(4),
                        border: Border.all(color: Colors.grey.shade500)),
                    child: RadioListTile(
                        contentPadding: const EdgeInsets.all(0),
                        title: const Text("Female"),
                        value: Gender.female,
                        groupValue: gender,
                        onChanged: (Gender? value) {
                          setState(() {
                            gender = value;
                          });
                        }))
              ],
            ),
            const SizedBox(height: 20),
            const Padding(
              padding: EdgeInsets.only(bottom: 5),
              child: Text("Date of Birth"),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                _textDateField(
                    first: true,
                    last: false,
                    maxLength: 2,
                    textWidth: 100,
                    textHint: 'dd'),
                const SizedBox(
                  width: 10,
                ),
                _textDateField(
                    first: false,
                    last: false,
                    maxLength: 2,
                    textWidth: 100,
                    textHint: 'mm'),
                const SizedBox(width: 10),
                _textDateField(
                    first: false,
                    last: true,
                    maxLength: 4,
                    textWidth: 100,
                    textHint: 'yyyy')
              ],
            )
          ],
        ),
      ),
    );
  }

  Widget _addressCardView() {
    return SizedBox(
        width: MediaQuery.of(context).size.width,
        child: Card(
            child: Padding(
          padding: const EdgeInsets.all(15.0),
          child: ElevatedButton(
              style: ButtonStyle(
                  elevation: MaterialStateProperty.all<double>(0),
                  padding: MaterialStateProperty.all<EdgeInsets>(
                      const EdgeInsets.all(13)),
                  side: MaterialStateProperty.all<BorderSide>(
                      const BorderSide(color: Colors.black26)),
                  backgroundColor:
                      MaterialStateProperty.all<Color>(Colors.white)),
              onPressed: () {
                openLocationBottomSheet();
              },
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(selectedCityText),
                  const Icon(Icons.location_pin)
                ],
              )),
        )));
  }

  Widget _textDateField(
      {required bool first,
      last,
      required int maxLength,
      required double textWidth,
      required String textHint}) {
    return SizedBox(
        height: 60,
        width: textWidth,
        child: AspectRatio(
          aspectRatio: 1,
          child: TextField(
            onChanged: (value) {
              if (value.length == maxLength && last == false) {
                FocusScope.of(context).nextFocus();
              }
              if (value.isEmpty && first == false) {
                FocusScope.of(context).previousFocus();
              }

              // if (value.isNotEmpty) {
              //   otpNumber.add(value);
              //   context.read<OTPBloc>().add(OTPEntered(otp: value));
              // }
            },
            showCursor: false,
            readOnly: false,
            textAlign: TextAlign.center,
            style: const TextStyle(fontSize: 16),
            keyboardType: TextInputType.number,
            maxLength: maxLength,
            decoration: InputDecoration(
                hintText: textHint,
                contentPadding: const EdgeInsets.all(0),
                counter: const Offstage(),
                enabledBorder: OutlineInputBorder(
                    borderSide:
                        const BorderSide(color: Colors.black12, width: 2),
                    borderRadius: BorderRadius.circular(10)),
                focusedBorder: OutlineInputBorder(
                    borderSide:
                        BorderSide(width: 2, color: Colors.cyan.shade500),
                    borderRadius: BorderRadius.circular(10))),
          ),
        ));
  }

  Future<void> openLocationBottomSheet() {
    return showModalBottomSheet(
        context: context,
        isScrollControlled: true,
        builder: (_) {
          return DraggableScrollableSheet(
              initialChildSize: 0.8,
              maxChildSize: 0.9,
              minChildSize: 0.2,
              expand: false,
              builder: (_, controller) {
                return Material(
                    elevation: 10,
                    borderRadius: const BorderRadius.vertical(
                      top: Radius.circular(20),
                    ),
                    color: Colors.white,
                    child: Padding(
                      padding: const EdgeInsets.all(2.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Align(
                            alignment: Alignment.topCenter,
                            child: Container(
                              margin: const EdgeInsets.only(top: 5, bottom: 20),
                              height: 6.0,
                              width: 70.0,
                              decoration: BoxDecoration(
                                  color: Colors.grey[400],
                                  borderRadius: BorderRadius.circular(10.0)),
                            ),
                          ),
                          const Padding(
                            padding: EdgeInsets.symmetric(horizontal: 12.0),
                            child: Text(
                              "Search your city",
                              style: TextStyle(
                                  fontWeight: FontWeight.bold, fontSize: 16),
                            ),
                          ),
                          Padding(
                            padding: const EdgeInsets.only(
                                top: 10, bottom: 0, left: 10, right: 10),
                            child: CupertinoSearchTextField(
                              placeholder: "Search here",
                              onChanged: (value) {
                                _debounce(() {
                                  registrationBloc.add(LocationSearchTextChange(
                                      searchString: value, page: 1));
                                });
                              },
                            ),
                          ),
                          BlocProvider(
                            create: (context) => registrationBloc,
                            child: BlocListener<RegistrationBloc,
                                RegistrationState>(listener: (context, state) {
                              final formState = state.locationSearchState;
                              if (formState is LocationSearchLoading) {
                                const CircularProgressIndicator();
                              } else {
                                const Text("Loading");
                              }
                            }, child: BlocBuilder<RegistrationBloc,
                                RegistrationState>(builder: (context, state) {
                              return Expanded(
                                  child: ListView.builder(
                                controller: controller,
                                scrollDirection: Axis.vertical,
                                shrinkWrap: true,
                                physics: const ScrollPhysics(),
                                itemCount: state.locationList.length,
                                itemBuilder: (context, index) {
                                  return ListTile(
                                    selected: selectedCityIndex == index
                                        ? true
                                        : false,
                                    title: Text(state
                                        .locationList[index].location
                                        .toString()),
                                    leading: const Icon(Icons.location_on),
                                    trailing: Visibility(
                                      visible: selectedCityIndex != index
                                          ? false
                                          : true,
                                      child: const Icon(
                                        Icons.done,
                                        color: Colors.black,
                                      ),
                                    ),
                                    selectedColor: Colors.black,
                                    selectedTileColor: Colors.grey.shade300,
                                    onTap: () {
                                      setState(() {
                                        selectedCityIndex = index;
                                        selectedCityText = state
                                            .locationList[index].location
                                            .toString();
                                        Navigator.of(context).pop();
                                      });
                                    },
                                  );
                                },
                              ));
                            })),
                          )
                        ],
                      ),
                    ));
              });
        });
  }
}
stact commented 2 years ago

@ZillionCAdmin please use code markdown to display your code. https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#quoting-code

https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks

ZillionCAdmin commented 2 years ago

@ZillionCAdmin please use code markdown to display your code. https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#quoting-code

https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks

I have already used. Still want me to do changes ?

stact commented 2 years ago

@ZillionCAdmin could you share also your bloc definition? Little tips you can also use BlocConsumer to prevent nested BlocListener and BlocBuilder :)

ZillionCAdmin commented 2 years ago

@stact

class RegistrationBloc extends Bloc<RegistrationEvent, RegistrationState> {
  final LocationSearchRepository locationSearchRepository;

  RegistrationBloc({required this.locationSearchRepository})
      : super(RegistrationState(locationList: [])) {
    on<LocationSearchTextChange>(_locationSearchTextChange);
    on<LocationOnLoadFetchData>(_locationSearchTextChange);
  }

  FutureOr<void> _locationSearchTextChange(event, emit) async {
    emit(state.copyWith(
        searchString: event.searchString,
        locationList: state.locationList,
        locationSearchState: LocationSearchLoading()));

    try {
      LocationCityModel result = await locationSearchRepository
          .findAndListLocation(event.searchString, event.page);
      if (result.status == 200) {
        emit(state.copyWith(
            locationList: result.location,
            locationSearchState:
                LocationSearchSuccess(locationCityModel: result)));
      } else {
        emit(state.copyWith(
            locationList: [],
            locationSearchState:
                LocationSearchFailed(exception: Exception(result.message))));
      }
    } catch (e) {
      emit(state.copyWith(
          locationList: [],
          locationSearchState:
              LocationSearchFailed(exception: e as Exception)));
    }
  }
}

Do i have nested BlocListener and BlocBuilder ?? Can u give me any example ? Thanks!

ZillionCAdmin commented 2 years ago

Screenshot 2022-01-09 at 6 50 41 PM

stact commented 2 years ago

@ZillionCAdmin

Here a sample for BlocConsumer

      BlocConsumer<YourBloc, YourState>(
        listener: (context, state) {
          // Do something
        },
        builder: (context, state) {
          return Something();
        },
      )
ZillionCAdmin commented 2 years ago

@stact Would it solve my issue? I'll still add this.

stact commented 2 years ago

@ZillionCAdmin it's strange, I don't see any misconfiguration. @felangel could you help us?

No BlocConsumer will not solve the issue, it's just a tips for you to improve code lisibility. @ZillionCAdmin I will try to reproduce your behavior with a small app and share with the community.

ZillionCAdmin commented 2 years ago

@stact Thank you so much for. your effort. I hope this will help others too. and just to let you know that from Main Page, I'm calling this Registration.dart page using RepositoryProvider.

Ex. Code:

RepositoryProvider(
                    child: const RegistrationStep(),
                    create: (context) => LocationSearchRepository());

Is there any issue in code which i have written ? or Bloc Related Issue ?

narcodico commented 2 years ago

@ZillionCAdmin can you please share a repo with your code instead of a bunch of code as text? it's saving us all time if we can run your sample. Thanks.

ZillionCAdmin commented 2 years ago

@narcodico Will try ASAP. Thanks

ZillionCAdmin commented 2 years ago

@stact @narcodico , Is it okay if i make private repo and add you as collaborator ?

ZillionCAdmin commented 2 years ago

@stact @narcodico , I have added you both as collaborator. Please check. Thank Again. :)

ZillionCAdmin commented 2 years ago

@narcodico Any update ?? I'm waiting for your response.

narcodico commented 2 years ago

You're not correctly making use of BlocProvider BlocProvider(create: (context) => registrationBloc,...) If you already have an instance you should use BlocProvider.value, when using create your bloc will be disposed automatically when bloc provider is unmounted, which is your case when closing the sheet. I'm not sure what you're trying to achieve with your code, but I suggest you split your tree into multiple smaller widgets instead of having everything inside a stateful one.

ZillionCAdmin commented 2 years ago

You're not correctly making use of BlocProvider BlocProvider(create: (context) => registrationBloc,...) If you already have an instance you should use BlocProvider.value, when using create your bloc will be disposed automatically when bloc provider is unmounted, which is your case when closing the sheet. I'm not sure what you're trying to achieve with your code, but I suggest you split your tree into multiple smaller widgets instead of having everything inside a stateful one.

Okay, So you're saying instead of BlocProvider with create, I should use BlocProvider.value but i think it gives me some other error then. I think i need to remove it from the top and create the bloc instance when required. would it be okay ?

ZillionCAdmin commented 2 years ago

@narcodico , Awesome Man, Thank you soooo much. You Saved my daysssss. thank you so much. Really you caught the right point. Even i was also going to do this but i thought this would be a wrong approach.

Working Now!!

Innocentkonan93 commented 2 years ago

@ZillionCAdmin Thank you much man, I solved my issue with your tip.

babaosoftware commented 2 years ago

You're not correctly making use of BlocProvider BlocProvider(create: (context) => registrationBloc,...) If you already have an instance you should use BlocProvider.value, when using create your bloc will be disposed automatically when bloc provider is unmounted, which is your case when closing the sheet.

You're a life saver. I had no idea BlocProvider closes the bloc automatically. I guess I never shared it before between multiple pages, so it didn't matter.

7Kronos commented 1 year ago

@felangel Thanks for the great library !

I understand that hiding the error could be dangerous for the exact problem described above. But...

I rely a lot on caching remote changes, I mutate the cache while sending the request to the backend and commit again with a refreshed result or rollback in case of error. So most of my usecases or repository methods are not simple Future's but Stream's.

The result is amazing as the responsiveness of the app is instant and UX as intended.

I wanted my of the cubits / blocs life cycle to be tied to the page main widget, so I often use Provider(create: or BlocBuilder(bloc: to instantiate them from IoC. I do want them to be disposed when the page widget is disposed.

But of course I will get this exception in case of remaining shenanigans occuring on the backend and a Future or a yield happen lately and I don't want to block the user from continuing its journey on my app.

I would have relaxed this error in my use case, as the main purpose of the async process is to commit / rollback the cache in case of remote shenanigans.

To prevent adding an insane amount of "if (isClosed) return;" in my blocs, what do you think about adding an argument to prevent the throwed exception or an "overload" like maybeEmit() ?

Example of code :

    emit(HomeState.loading());

    await for (var result in userDisplayProducts()) {
      result.fold(
        (error) => emit(HomeState.error(message: error.message)),
        (data) {
          if (isClosed) return;
          emit(
            HomeState.success(
              entries: data.entries,
            ),
          );
        },
      );
    }
silkstream-tristan commented 1 year ago

I have a similar issue but my Bloc code structure throws an error when using the add method of the Bloc class.

Everything worked fine prior to this issue being introduced with version 8.0.0 of Flutter_Bloc where the add method rethrows the error from the _eventController property which is a Flutter StreamController.

https://github.com/felangel/bloc/blob/aade25853c7e83225a5b844c5c17e1c901949e7c/packages/bloc/lib/src/bloc.dart#L268-L274

The app I'm working on is being upgraded to the latest version of Flutter which is why I updated the Bloc package to 8.1.3 from 7.2.0.

The app is using a Bloc per page / route where the Bloc is created when the page is created and would close events when the page / route is removed.

To reproduce the issue:

  1. Create two pages / routes where the back button on Android is supported. You will need a button on the first route to go to the second route. The second route will need a button to send the network request.
  2. Create a Bloc for the second route which is created when the route page widget is created, not globally.
  3. Create an event called sendRequest in the Bloc that will contain a network request that could take 5 seconds. You might be able to substitute this with await Future.delayed(Duration(seconds:5);.
  4. At the end of the event, there is a call to add another event to indicate a completion state.
  5. If you tap the back button while the sentRequest event is running, the add method will flag an error.

I'll see if I can put this together as a demo.

As a workaround, I implemented an override of the add method to every Bloc class I created:

import 'package:flutter_bloc/flutter_bloc.dart';

import 'splash_event.dart';
import 'splash_state.dart';

class SplashBloc extends Bloc<SplashEvent, SplashState> {
  SplashBloc() : super(SplashState.active());

  @override
  void add(event) {
    if (!isClosed) {
      super.add(event);
    }
  }
}

I'm not sure what the real solution would be but as @7Kronos suggests, you could have an option on the Bloc methods to silence particular errors.

Making these page Blocs global might not be possible because a Bloc might need to be used on more than one page of a path. Also, the Blocs should be disposed of when they're not needed, I don't want to create 30+ page Blocs listening to events when I'm viewing the home page of the app.

7Kronos commented 1 year ago

@silkstream-tristan I ended up with the same solution for now because I started to have metric tonnes of if (isClosed) return; in my app.

The main issue is that if you don't want to prevent navigation while loading or refreshing data, you will face this scenario.

An extra parameter in emit() function to silence the error seems to be a good solution.