Search code examples
flutterdartblocflutter-bloc

Concepts of BlocListener, BlocBuilder, and BlocProvider


I am having difficulty understanding this structure. My idea is simple: I have a MultiBlocProvider in my build with two BlocProviders. The first one uses AuthBloc and immediately calls my GetToken, and the second one prepares my provider (TokenBloc).

@override
Widget build(BuildContext context) {
  return MultiBlocProvider(
    providers: [
      BlocProvider<AuthBloc>(
        create: (context) => sl()..add(const GetToken()),
      ),
      BlocProvider<TokenBloc>(
        create: (context) => sl<TokenBloc>(),
      ),
    ],
    child: ...
  );
}

In my StatefulWidget:

body: BlocBuilder<AuthBloc, AuthBlocState>(
  builder: (_, state) {
    if (state is AuthLoading) {
      return const Center(child: CupertinoActivityIndicator());
    }
    if (state is AuthError) {
      return const Center(child: CupertinoActivityIndicator());
    }
    if (state is AuthDone) {
      return Container() // #MORE
    }
    ...
  }
)

So far, everything is fine. My container is only displayed when the state is AuthDone. However, in my widget, at #MORE, I have a button that should trigger (onTap) a method called _go():

_go() async {
  BlocProvider.of<TokenBloc>(context).add(const GetToken());
}

Here everything is great; it works. However, I wanted to create a listener to track the states and at the same time execute other functions (without involving the creation of widgets). As far as I understood, the step was:

go() async {
  BlocProvider<TokenBloc>(
    create: (context) => sl()..add(const GetToken()),
    child: BlocListener<TokenBloc, TokenBlocState>(
      listenWhen: (previousState, currentState) {
        debugPrint("E1");
        debugPrint(currentState.data!.toString());
        return currentState is TokenDone ||
            currentState is TokenLoading;
      },
      listener: (_, state) {
        debugPrint(state.toString());
        if (state is TokenLoading) {
          debugPrint("LOADING");
        }
        if (state is TokenError) {
          debugPrint("ERROR");
        }
        if (state is TokenDone) {
          debugPrint("DONE");
        }
      },
    ),
  );
}

With this change, it no longer triggers my API or the BlocListener. This method needs to be separated from the widget because it will do something else.

I really thought I was following the right path. Any help is welcome.


Solution

  • The BlocListener is a widget. It needs to be in the widget tree to properly work, the widget tree being what you return in the build method of your widgets (or their states in case of stateful widgets).

    Under the hood, BlocListener gets the bloc/cubit from the context in initState and listens on it, applies the condition listenWhen and calls the listener method.

    If you want to create a listener on your cubit/bloc outside of the widget-tree, like on a button click, you can do it like you would with the Stream:

    BlocProvider.of<TokenBloc>(context).stream.listen((state) {
      // state is of TokenBlocState type
    });
    

    The listen method returns a StreamSubscription that you need to cancel when you no longer need the subscription.

    But I see you're listening in the (what I assume is) some button callback. You probably want to check the current state of the bloc, not listen for the changes. Then you can just get the state of the bloc like so:

    final state = BlocProvider.of<TokenBloc>(context).state;
    // state is of TokenBlocState type
    

    Regarding the BlocProvider widget that you put above BlocListener in your go() method - you probably want to use the bloc you already provided somewhere else, higher in the widget tree, not create a new instance. If the BlocProvider was outside of your widget, you can get it through BlocProvider.of(context) like in the example above. If it is in the same widget as the method, you need to pass to the .of() method a BuildContext that contains this provider, so wrap your widget with Builder widget and pass the context to your go() method.