BlocProvider.of() called with a context that does not contain a Bloc - even that it does
Asked Answered
M

6

50

First of, I do know how BLoC suppose to work, the idea behind it and I know the difference between BlocProvider() and BlocProvider.value() constructors.

For simplicity, my application has 3 pages with a widget tree like this:

App() => LoginPage() => HomePage() => UserTokensPage()

I want my LoginPage() to have access to UserBloc because i need to log in user etc. To do that, I wrap LoginPage() builder at App() widget like this:

void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: BlocProvider<UserBloc>(
        create: (context) => UserBloc(UserRepository()),
        child: LoginPage(),
      ),
    );
  }
}

That obviously works just fine. Then, if User logs in successfully, he is navigated to HomePage. Now, I need to have access to two different blocs at my HomePage so I use MultiBlocProvider to pass existing UserBloc further and create a brand new one named DataBloc. I do it like this:

  @override
  Widget build(BuildContext context) {
    return BlocListener<UserBloc, UserState>(
      listener: (context, state) {
        if (state is UserAuthenticated) {
          Navigator.of(context).push(
            MaterialPageRoute<HomePage>(
              builder: (_) => MultiBlocProvider(
                providers: [
                  BlocProvider.value(
                    value: BlocProvider.of<UserBloc>(context),
                  ),
                  BlocProvider<DataBloc>(
                    create: (_) => DataBloc(DataRepository()),
                  ),
                ],
                child: HomePage(),
              ),
            ),
          );
        }
      },
[...]

This also works. Problem happens when from HomePage user navigates to UserTokensPage. At UserTokensPage I need my already existing UserBloc that I want to pass with BlocProvider.value() constructor. I do it like this:

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: false,
        title: Text('My App'),
        actions: <Widget>[
          CustomPopupButton(),
        ],
      ),

[...]

class CustomPopupButton extends StatelessWidget {
  const CustomPopupButton({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      icon: Icon(Icons.more_horiz),
      onSelected: (String choice) {
        switch (choice) {
          case PopupState.myTokens:
            {
              Navigator.of(context).push(
                MaterialPageRoute<UserTokensPage>(
                  builder: (_) => BlocProvider.value(
                    value: BlocProvider.of<UserBloc>(context),
                    child: UserTokensPage(),
                  ),
                ),
              );
            }
            break;
          case PopupState.signOut:
            {
              BlocProvider.of<UserBloc>(context).add(SignOut());
              Navigator.of(context).pop();
            }
        }
      },
[...]

When I press button to navigate to MyTokensPage i get error with message:

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building Builder(dirty):
        BlocProvider.of() called with a context that does not contain a Bloc of type UserBloc.

        No ancestor could be found starting from the context that was passed to BlocProvider.of<UserBloc>().

        This can happen if:
        1. The context you used comes from a widget above the BlocProvider.
        2. You used MultiBlocProvider and didn't explicity provide the BlocProvider types.

        Good: BlocProvider<UserBloc>(create: (context) => UserBloc())
        Bad: BlocProvider(create: (context) => UserBloc()).

        The context used was: CustomPopupButton

What am I doing wrong? Is it because i have extracted PopupMenuButton widget that somehow loses blocs? I don't understand what I can be doing wrong.

Muhammadan answered 24/1, 2020 at 19:10 Comment(3)
Why you need a BlocListener to navigate to the next page? I think this can be accomplished in a simpler way using BlocBuilder and return the page you need to return when UserAuthenticatedSpirochaetosis
@StefanoSaitta I did it this way because thats what official flutter_bloc documentation for BlocListeners recomends to do.Muhammadan
@Muhammadan give my answer a try and let me know of the result.Holinshed
M
34

EDIT 10/03/2022

Since this thread became very popular I feel I need to add some comments.

This is valid solution if your goal is to use blocs that are not provided above your MaterialApp widget, but instead being declared somewhere down the widget tree by wrapping your widget (eg. some page) with BlocProvider making it possible for that widget to access the bloc.

It is easier to avoid problems by declaring all your blocs in MultiBlocProvider somewhere up the widget tree (like I said before), but this topic was not created with that in mind. Feel free to upvote and use this aproach described in Amesh Fernando response but do that knowing the difference.


I fixed it. Inside App widget i create LoginPage with

home: BlocProvider<UserBloc>(
        create: (context) => UserBloc(UserRepository()),
        child: LoginPage(),

At LoginPage I simply wrap BlocBuilders one into another

Widget build(BuildContext context) {
    return BlocListener<UserBloc, UserState>(
      listener: (context, state) {
        if (state is UserAuthenticated) {
          Navigator.of(context).push(
            MaterialPageRoute<HomePage>(
              builder: (_) => BlocProvider.value(
                value: BlocProvider.of<UserBloc>(context),
                child: BlocProvider<NewRelicBloc>(
                  create: (_) => NewRelicBloc(NewRelicRepository()),
                  child: HomePage(),
                ),
              ),
            ),
          );
        }
      },
[...]

PopupMenuButton navigates User to TokenPage with

              Navigator.of(context).push(
                MaterialPageRoute<UserTokensPage>(
                  builder: (_) => BlocProvider.value(
                    value: BlocProvider.of<UserBloc>(context),
                    child: UserTokensPage(),
                  ),
                ),
              );

And that solved all my problems.

Muhammadan answered 29/1, 2020 at 15:27 Comment(1)
It's worth noting that using the name context for every context object can really confuse things (that's why we name our variables clearly and cleanly). I've found naming each context separately helps track down where there confusion may be with providers. Your underscore approach worked but may still cause confusion if you end up with nested underscore contexts.Lief
N
37

You can just wrap the Blocs you need to access through out the app by wrapping it at the entry point of the app like this

  runApp(
      MultiBlocProvider(
          providers: [
            BlocProvider<UserBloc>(
              create: (context) =>
                  UserBloc(UserRepository()),
            ),

          ],
          child: App()
      )
  );
}

and you can access this bloc at anywhere of your app by

BlocProvider.of<UserBloc>(context).add(event of user bloc());

Novosibirsk answered 20/2, 2020 at 9:30 Comment(6)
Do I have to implement each Bloc in this way in main.dart file? Can you explain why should we do this? Your answer has saved me. Thank youUndernourished
No, but you can't provide it on the current page(the page you are currently at). Either you can provide it before navigating to a specific page or on the main.dart file.Novosibirsk
Its simple and real solution. Thanks!Gisela
Damn simple solution. Its a hell working with those context.Schaffel
@AmeshFernando you actually can provide it on current page by using builder between blocProvider and BlocListenerTempura
@AmeshFernando. thanks. moving the bloc provider(s) up from material app did the trickVolute
M
34

EDIT 10/03/2022

Since this thread became very popular I feel I need to add some comments.

This is valid solution if your goal is to use blocs that are not provided above your MaterialApp widget, but instead being declared somewhere down the widget tree by wrapping your widget (eg. some page) with BlocProvider making it possible for that widget to access the bloc.

It is easier to avoid problems by declaring all your blocs in MultiBlocProvider somewhere up the widget tree (like I said before), but this topic was not created with that in mind. Feel free to upvote and use this aproach described in Amesh Fernando response but do that knowing the difference.


I fixed it. Inside App widget i create LoginPage with

home: BlocProvider<UserBloc>(
        create: (context) => UserBloc(UserRepository()),
        child: LoginPage(),

At LoginPage I simply wrap BlocBuilders one into another

Widget build(BuildContext context) {
    return BlocListener<UserBloc, UserState>(
      listener: (context, state) {
        if (state is UserAuthenticated) {
          Navigator.of(context).push(
            MaterialPageRoute<HomePage>(
              builder: (_) => BlocProvider.value(
                value: BlocProvider.of<UserBloc>(context),
                child: BlocProvider<NewRelicBloc>(
                  create: (_) => NewRelicBloc(NewRelicRepository()),
                  child: HomePage(),
                ),
              ),
            ),
          );
        }
      },
[...]

PopupMenuButton navigates User to TokenPage with

              Navigator.of(context).push(
                MaterialPageRoute<UserTokensPage>(
                  builder: (_) => BlocProvider.value(
                    value: BlocProvider.of<UserBloc>(context),
                    child: UserTokensPage(),
                  ),
                ),
              );

And that solved all my problems.

Muhammadan answered 29/1, 2020 at 15:27 Comment(1)
It's worth noting that using the name context for every context object can really confuse things (that's why we name our variables clearly and cleanly). I've found naming each context separately helps track down where there confusion may be with providers. Your underscore approach worked but may still cause confusion if you end up with nested underscore contexts.Lief
K
14

Change name of context in builder whether in bottomSheet or materialPageRoute.

So that bloc can access parent context through context unless it's going to take context from builder (bottom sheet). This can lead to an error which you can't reach the instance of bloc .

showModalBottomSheet(
     context: context,
     builder: (context2) {  ===> change here to context2
     BlocProvider.value(
          value: BlocProvider.of<BlocA>(context),
          child: widgetA(),
                       ),
     }
Kremlin answered 25/3, 2021 at 4:57 Comment(4)
This is so helpful. Thank you!Nica
this actually the rootcasue of my problem, thanksSlippage
This answer is underrated. It was the solution I was looking for. Kudos.Chatelain
Thanks for this answer. It's easy to lose track of which context is being passed around.Kob
H
7

Solution

Method A: Access UserBloc provider instance directly without passing it

I prefer this solution since it requires less code.

A.1 Wrap CustomPopupButton instance with provider Consumer so it rebuilds itself whenever UserBloc notifies listeners of value changes.

Change this:

actions: <Widget>[
    CustomPopupButton(),
],

To:

actions: <Widget>[
    Consumer<UserBloc>(builder: (BuildContext context, UserBloc userBloc, Widget child) {
      return CustomPopupButton(),
    });
],

A.2 Change Provider instance invocation inside the stateless widget to disable listening to value changes -- "listening" and resulting "rebuilds" are already done by Consumer.

A.2.1 Change this:

value: BlocProvider.of<UserBloc>(context),

To:

value: BlocProvider.of<UserBloc>(context, listen: false),

A.2.2 And change this:

BlocProvider.of<UserBloc>(context).add(SignOut());

To:

BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());

Method B: pass UserBloc provider instance

Same thing as Method A, but:

  • In A.1 you'd pass userBloc like this: return CustomPopupButton(userBloc: userBloc),.
  • You'd declare final UserBloc userBloc; member property inside CustomPopupButton.
  • In A.2 you'd do this: userBloc.add(SignOut()); instead of BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());

Explanation

flutter_bloc is using Provider, to be aware what's going on it's better understand Provider. Please refer to my answer here to understand my answer to your question, and to understand Provider and listen flag better.

Holinshed answered 25/1, 2020 at 11:36 Comment(5)
Give this solution a try and let me know if it worked. Can't test it. It'd be great if you'd provided your Provider types implementations.Holinshed
I tried to implement your solution. Unfortunately, flutter_bloc Provider.of<T>(context) does not have listen parameter which i can set to false as said at A.2.1. Furthermore, after wrapping CustomPopupButton with ConsumerBloc (without changing Provider.of() ) CustomPopup widget crashes and does not render because "BlocProvider.of() called with a context that does not contain a Bloc of type UserBloc.". It seems UserBloc creation fails already at LoginPage but due MultiBlocProvider it doesnt crash app.Muhammadan
@Muhammadan Well you're in luck. flutter_bloc is already doing that. So you don't need to specify listen: false as it's already done for you. I'll update my answer when I get to with some possible fixes and notify you when done.Holinshed
would you mind to help me on this? #73150200Rosinarosinante
@MarcelDz I see it was already answered, glad it did.Holinshed
R
3

You need to either decompose your widget into two widgets (which I recommend for testability reasons) or use a Builder widget to get a child context.

class MyHomePage extends StatelessWidget { 
  @override Widget build(BuildContext context) {
     return BlocProvider( create: (_) => TestCubit(), child: MyHomeView(), ); 
  }
}

class MyHomeView extends StatelessWidget {
  @override Widget build(BuildContext context) { 
    return Scaffold( body: Center( child: RaisedButton(onPressed: () => BlocProvider.of<TestCubit>(context)...) ), ); 
    } 
  }

source: solved by Felix Angelov, https://github.com/felangel/bloc/issues/2064

Roncesvalles answered 28/5, 2021 at 12:18 Comment(0)
G
0

you don't have to use BlocProvider.value() to navigate to another screen, you can just wrap MaterialApp into BlocProvider as a child of it

Godbeare answered 18/2, 2020 at 21:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.