Bloc: Bloc object destroyed even though reference is in parent class

Created on 24 Jun 2020  ·  6Comments  ·  Source: felangel/bloc

I created my custom NavDrawer using Stateful widget and added Bloc to it so I can dynamically update UI depending on if a user changed the profile picture or updated any other data that is present in NavDrawer.

The problem:
When I close NavDrawer it seems that the bloc object is destroyed. Bloc object is created for drawer in parent class/screen that is never removed from the stack, so the reference of the Bloc object is always available until the app is closed. So, from my understanding, the NavDrawer should always have the same bloc and last bloc state because the instance of the bloc has never changed because the instance is in the parent widget that is still displayed on the device.
And yet I am getting an error when I am trying to perform logout or get other details from the server with error: _Unhandled error Bad state: Cannot add new events after calling close occurred in bloc Instance of 'NavigationDrawerBloc'._

My question is why the bloc object is destroyed? Shouldn't it be passed over and over again to the screen even though the screen is destroyed and recreated? Is this a bug or expected behavior? If it is expected, please advise me on how to keep bloc alive even when screen is destroyed? And how to pass the same bloc to a newly created screen?

Here is CustomNavDrawer:

class CustomNavigationDrawer extends StatefulWidget {
  @override
  _CustomNavigationDrawerState createState() => _CustomNavigationDrawerState();
}

class _CustomNavigationDrawerState extends State<CustomNavigationDrawer> {
  MobileSpecificModel mobileSpecificModel = GetIt.instance.get<MobileSpecificModel>();
  UserDto _userData;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<NavigationDrawerBloc, NavigationDrawerState>(builder: (builderContext, state) {
      if (state is DrawerLoadingState) {
        return LoadingWidget();
      } else if (state is DrawerLoadedState) {
        _userData = state.userDto;
        return Drawer(
          child: Container(
            color: AppColors.NAV_DRAWER_BODY_BACKGROUND_COLOR,
            child: ListView(
              padding: EdgeInsets.zero,
              children: <Widget>[
                CustomNavigationDrawerHeader(_userData),
                SizedBox(
                  height: 10,
                ),
                ListTile(
                  leading: Text(
                    AppStrings.NavDrawerProfileSettingsTitle,
                    style: AppFontStyles.kNavDrawerListTileTextStyle,
                  ),
                  trailing: ImageIcon(
                    AssetImage(
                      AppImages.IcMenuSettings,
                    ),
                    color: Colors.white,
                  ),
                  onTap: _openProfileSettings,
                ),
                Divider(
                  height: 20,
                  color: AppColors.NAV_DRAWER_HEADER_BACKGROUND_COLOR,
                  thickness: 2,
                ),
                ListTile(
                  leading: Text(
                    AppStrings.NavDrawerAboutUsTitle,
                    style: AppFontStyles.kNavDrawerListTileTextStyle,
                  ),
                  trailing: ImageIcon(
                    AssetImage(
                      AppImages.IcMenuAbout,
                    ),
                    color: Colors.white,
                  ),
                  onTap: () {
                    _openUrl(mobileSpecificModel.urlFrontendAboutUs);
                  },
                ),
                ListTile(
                  leading: Text(
                    AppStrings.NavDrawerContactUsTitle,
                    style: AppFontStyles.kNavDrawerListTileTextStyle,
                  ),
                  trailing: ImageIcon(
                    AssetImage(
                      AppImages.IcMenuMessage,
                    ),
                    color: Colors.white,
                  ),
                  onTap: () {
                    _openUrl(mobileSpecificModel.urlFrontendContactUs);
                  },
                ),
                ListTile(
                  leading: Text(
                    _userData != null ? AppStrings.NavDrawerLogoutTitle : AppStrings.NavDrawerLoginTitle,
                    style: AppFontStyles.kNavDrawerListTileTextStyle,
                  ),
                  trailing: ImageIcon(
                    AssetImage(
                      AppImages.IcMenuLogout,
                    ),
                    color: Colors.white,
                  ),
                  onTap: _userData != null ? _performLogOut : _navigateToLogin,
                ),
              ],
            ),
          ),
        );
      } else if (state is DrawerNotLoggedInState) {
        return Drawer(
          child: Container(
            color: AppColors.NAV_DRAWER_BODY_BACKGROUND_COLOR,
            child: ListView(
              padding: EdgeInsets.zero,
              children: <Widget>[
                CustomNavigationDrawerHeader(_userData),
                SizedBox(
                  height: 10,
                ),
                ListTile(
                  leading: Text(
                    _userData != null ? AppStrings.NavDrawerLogoutTitle : AppStrings.NavDrawerLoginTitle,
                    style: AppFontStyles.kNavDrawerListTileTextStyle,
                  ),
                  trailing: ImageIcon(
                    AssetImage(
                      AppImages.IcMenuLogout,
                    ),
                    color: Colors.white,
                  ),
                  onTap: _userData != null ? _performLogOut : _navigateToLogin,
                ),
              ],
            ),
          ),
        );
      } else {
        return Center(
          child: Text(
            'ERROR',
            style: AppFontStyles.WhiteFont20,
          ),
        );
      }
    });
  }

  void _navigateToLogin() {
    Navigator.pushNamed(context, MainLoginScreen.id);
  }

  void _performLogOut() {
    BlocProvider.of<NavigationDrawerBloc>(context).add(DrawerLogoutEvent());
    _navigateToLogin();
  }

  void _openProfileSettings() {
    if (_userData != null) Navigator.pushNamed(context, ProfileSettingsScreen.id, arguments: ProfileSettingsScreenArgs(_userData));
  }

  void _openUrl(String url) async {
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw 'Could not launch $url';
    }
  }
}

class ProfileSettingsScreenArgs {
  final UserDto userData;

  ProfileSettingsScreenArgs(this.userData);
}

Here is a code snippet of build method from parent widget where NavDrawer and bloc are instantiated:

 Widget build(BuildContext context) {
    return Scaffold(
      key: widget._scaffoldStateContainingDrawerKey,
      drawer: BlocProvider(
        create: (BuildContext context) => drawerBloc,
        child: CustomNavigationDrawer(),
      ),

Here is my flutter doctor -v:

$ flutter doctor -v 
[✓] Flutter (Channel stable, v1.17.4, on Mac OS X 10.15.5 19F101, locale en-RS)
    • Flutter version 1.17.4 at /Users/lazarjovicic/flutter
    • Framework revision 1ad9baa8b9 (6 days ago), 2020-06-17 14:41:16 -0700
    • Engine revision ee76268252
    • Dart version 2.8.4


[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at /Users/lazarjovicic/Library/Android/sdk
    • Platform android-29, build-tools 29.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.5)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 11.5, Build version 11E608c
    • CocoaPods version 1.8.4

[✓] Android Studio (version 4.0)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 46.0.2
    • Dart plugin version 193.7361
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)

[✓] VS Code (version 1.46.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.11.0

[✓] Connected device (2 available)
    • SM G970F                  • RF8M728WTMP   • android-arm64 • Android 10 (API 29)
    • Android SDK built for x86 • emulator-5554 • android-x86   • Android 10 (API 29) (emulator)

• No issues found!
flutter_bloc question

Most helpful comment

@marcin-jelenski yup, thanks for bringing this up! I'll be sure to state this more explicitly in the upcoming documentation enhancements 👍

All 6 comments

Hi @lazarvgd 👋
Thanks for opening an issue!

I believe the problem is you're passing an instance of an already created bloc via the create of a BlocProvider. The way BlocProvider works is when the BlocProvider itself is unmounted from the widget tree, it will also handle disposing the bloc which was "created". In this case, you are maintaining a reference to the bloc outside of the BlocProvider which you should avoid. I would highly recommend creating the bloc within the create of BlocProvider like:

BlocProvider(
  create: (context) => DrawerBloc(),
  child: ...
),

If you need the bloc higher up in the widget tree then you can lift the BlocProvider up higher in the widget tree. The general rule of thumb is create/provide blocs at the lowest common ancestor of all widgets that need access to the bloc.

Hope that helps 👍

Hello @felangel, thanks for your answer. I think that you have misunderstood me. The code snippet

Widget build(BuildContext context) {
    return Scaffold(
      key: widget._scaffoldStateContainingDrawerKey,
      drawer: BlocProvider(
        create: (BuildContext context) => drawerBloc,
        child: CustomNavigationDrawer(),
      ),

Is actuall parent widget where bloc and NavDrawer are instantiated, and this screen is main sceren of the app. The only way to dispose this screen to close the application.
So, I did what you have mentioned, but anyway bloc is destroyed when Drawer is closed and parent widget is present all the time. I will create sample app in order to demonstrate this issue. Currently I am typing from phone.

Hello @felangel, thanks for your answer. I think that you have misunderstood me. The code snippet

Widget build(BuildContext context) {
    return Scaffold(
      key: widget._scaffoldStateContainingDrawerKey,
      drawer: BlocProvider(
        create: (BuildContext context) => drawerBloc,
        child: CustomNavigationDrawer(),
      ),

Is actuall parent widget where bloc and NavDrawer are instantiated, and this screen is main sceren of the app. The only way to dispose this screen to close the application.
So, I did what you have mentioned, but anyway bloc is destroyed when Drawer is closed and parent widget is present all the time. I will create sample app in order to demonstrate this issue. Currently I am typing from phone.

Hi @lazarvgd , maybe you are looking for "BlocProvider.value". You can read about it here https://pub.dev/packages/flutter_bloc

Instead of creating BlocProvider, you can rebuild it with BlocProvider.value and context what you already have.

I've had the same case - bloc wasn't destroyed properly when a reference was stored outside of create function. It was quite confusing - @felangel it might be worth mentioning in the docs that it should be avoided. I've found pretty much samples and articles which does the mistake. And that's a source of leaks - bloc isn't closed, subscriptions aren't disposed.

@lazarvgd I've noticed that making a bloc with disabled lazy flag fixes the issue. Close is called properly then.

    final bloc = _buildBloc(context);
    return BlocProvider<T>(
      create: (_) => bloc,
      child: _buildView(context, bloc),
      lazy: false, // notice a flag
    );

Internally, InheritedProvider disposes a descendant when a value is assigned. If it's lazy, it cannot be reached sometimes. Check _CreateInheritedProviderState and _didInitValue flag.

@marcin-jelenski yup, thanks for bringing this up! I'll be sure to state this more explicitly in the upcoming documentation enhancements 👍

Well done mr @felangel, thank you 🥇

Was this page helpful?
0 / 5 - 0 ratings

Related issues

komapeb picture komapeb  ·  3Comments

nhwilly picture nhwilly  ·  3Comments

hivesey picture hivesey  ·  3Comments

timtraversy picture timtraversy  ·  3Comments

shawnchan2014 picture shawnchan2014  ·  3Comments