Search code examples
flutterdartbloc

Is BlocProvider.value redundant?


I am learning Bloc Pattern in Flutter for state management and routing. I am taking an online course. According to the course, we must provide the existing Bloc or Cubit to the newly created subtrees( In navigation parts Navigator.push...). I read the documentation and found out that BlocProvider.value must be used as it really said. BlocProvider Doucmentation

However, I could provide the Cubit to newly created subtrees without using BlocProvider.value. What am I doing wrong? Or should I say, what am I doing right without knowing it :)?

main.dart routes

  routes: {
          "/": (context) => const MyHomePage(title: 'Flutter Demo Home Page'),
          "/second": (context) => const SecondScreen(title: "Second screen"),
          "/third": (context) => const ThirdScreen(title: "Third screen")
        },

HomeScreen



class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: BlocConsumer<CounterCubit, CounterState>(
        listener: (context, state) {
          if (state.isIncremented) {
            ScaffoldMessenger.of(context)
                .showSnackBar(const SnackBar(content: Text("Incremented")));
          } else {
            ScaffoldMessenger.of(context)
                .showSnackBar(const SnackBar(content: Text("Decremented")));
          }
        },
        builder: (context, state) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                BlocBuilder<CounterCubit, CounterState>(
                  builder: (context, state) {
                    return Text(
                      '${state.counterValue}',
                      style: Theme.of(context).textTheme.headlineMedium,
                    );
                  },
                ),
                Row(
                  children: [
                    FloatingActionButton(
                      heroTag: UniqueKey(),
                      onPressed: () {
                        BlocProvider.of<CounterCubit>(context).decrement();
                      },
                      tooltip: 'Increment',
                      child: const Icon(Icons.text_decrease),
                    ),
                    FloatingActionButton(
                      heroTag: UniqueKey(),
                      onPressed: () {
                        BlocProvider.of<CounterCubit>(context).increment();
                      },
                      tooltip: 'Increment',
                      child: const Icon(Icons.add),
                    ),
                  ],
                ),
                MaterialButton(
                  onPressed: () {
                    Navigator.pushNamed(context, '/second');
                  },
                  color: Colors.blue,
                  child: const Text("Navigate to Second Screen"),
                ),
                MaterialButton(
                  onPressed: () {
                    Navigator.pushNamed(context, '/third');
                  },
                  color: Colors.blue,
                  child: const Text("Navigate to Third Screen"),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

SecondScreen

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cubit/counter_cubit.dart';

class SecondScreen extends StatefulWidget {
  const SecondScreen({super.key, required this.title});

  final String title;

  @override
  State<SecondScreen> createState() => _SecondScreenState();
}

class _SecondScreenState extends State<SecondScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: BlocConsumer<CounterCubit, CounterState>(
        listener: (context, state) {
          if (state.isIncremented) {
            ScaffoldMessenger.of(context)
                .showSnackBar(const SnackBar(content: Text("Incremented")));
          } else {
            ScaffoldMessenger.of(context)
                .showSnackBar(const SnackBar(content: Text("Decremented")));
          }
        },
        builder: (context, state) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                BlocBuilder<CounterCubit, CounterState>(
                  builder: (context, state) {
                    return Text(
                      '${state.counterValue}',
                      style: Theme.of(context).textTheme.headlineMedium,
                    );
                  },
                ),
                Row(
                  children: [
                    FloatingActionButton(
                      heroTag: UniqueKey(),
                      onPressed: () {
                        BlocProvider.of<CounterCubit>(context).decrement();
                      },
                      tooltip: 'Increment',
                      child: const Icon(Icons.text_decrease),
                    ),
                    FloatingActionButton(
                      heroTag: UniqueKey(),
                      onPressed: () {
                        BlocProvider.of<CounterCubit>(context).increment();
                      },
                      tooltip: 'Increment',
                      child: const Icon(Icons.add),
                    ),
                  ],
                )
              ],
            ),
          );
        },
      ),
    );
  }
}

ThirdScreen

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cubit/counter_cubit.dart';

class ThirdScreen extends StatefulWidget {
  const ThirdScreen({super.key, required this.title});

  final String title;

  @override
  State<ThirdScreen> createState() => _ThirdScreenState();
}

class _ThirdScreenState extends State<ThirdScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: BlocConsumer<CounterCubit, CounterState>(
        listener: (context, state) {
          if (state.isIncremented) {
            ScaffoldMessenger.of(context)
                .showSnackBar(const SnackBar(content: Text("Incremented")));
          } else {
            ScaffoldMessenger.of(context)
                .showSnackBar(const SnackBar(content: Text("Decremented")));
          }
        },
        builder: (context, state) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                BlocBuilder<CounterCubit, CounterState>(
                  builder: (context, state) {
                    return Text(
                      '${state.counterValue}',
                      style: Theme.of(context).textTheme.headlineMedium,
                    );
                  },
                ),
                Row(
                  children: [
                    FloatingActionButton(
                      heroTag: UniqueKey(),
                      onPressed: () {
                        BlocProvider.of<CounterCubit>(context).decrement();
                      },
                      tooltip: 'Increment',
                      child: const Icon(Icons.text_decrease),
                    ),
                    FloatingActionButton(
                      heroTag: UniqueKey(),
                      onPressed: () {
                        BlocProvider.of<CounterCubit>(context).increment();
                      },
                      tooltip: 'Increment',
                      child: const Icon(Icons.add),
                    ),
                  ],
                )
              ],
            ),
          );
        },
      ),
    );
  }
}


Solution

  • I will say that you need to understand more about how an InheritedWidget, Navigator subtrees, the BuildContext works in Flutter and how this relates to your topic.

    in Flutter, the MaterialApp, is the highest component in your app, right?

    so by wrapping it with a BlocProvider, you're offering the whole app with that bloc/cubit, so I will explain with arrow structure to make you understand what's happening.

    • I will refer to "BlocProvider" as a BlocProvider of an example BlocA.
    • I will refer to a Navigator first screen subtree with SubTreeOne.
    • I will refer to a Navigator second screen subtree with SubTreeTwo.

    when you wrap MaterialApp with a BlocProvider:

                                              -> SubTreOne
                                             |
    BlocProvider -> MaterialApp(Navigator) ->   
                                             |
                                              -> SubTreeTwo
    

    In this example, when you will try to access the BlocA from any of your screens (subtrees), you will not have to pass it with a BlocProvider.value, because the BlocA will always be looked up and available using the context where your widget-tree exists, so by calling Navigator.push(), a new subtree will be put as a child of the Navigator, and when trying to use the BlocA from that screen/route, it will lookup for it using the BuildContext from where it exists (in that case, the Navigator's context), which means that looking up from there, it will find that BlocA because it is at the topmost level of your app using the BlocProvider.

    with this approach, you guarantee that your BlocA will be initialized only once in your app, available in the whole app, and will not be closed in your all your app cycle, until you either close your app or close the bloc manually using the close() method. so you might need this approach for handling authentication status of your users in the app, but not for using a bloc in a single screen or widget (at least, if you really care about perfermance and runtime memory of your app).

    Now, let's say you have a screen that shows a simple counter that you want to re-initialize every time that screen is taken and put off again in your widget tree then you want to have the option to pass that counter value to another screen, here you will have a structure like this:

                             -> BlocProvider -> SubTreeOne
                             |
    MaterialApp(Navigator) ->   
                             |
                              -> SubTreeTwo
    
                  
    

    at this example now, the BlocA is accesible in the SubTreeOne and working very fine, and each time you Navigator.pop(context) and Navigator.push(...SubTreeOne()), the bloc will be re-initialized again, as we expect, right? but when we do try to navigate to SubTreeTwo() now, you will notice that the BlocA at this point do not exists in the subtree, and you will have the most Bloc popular exception thrown in your app:

    BlocProvider.of<BlocA>() called with a context that does not contain a Bloc of type BlocA.
    
    No ancestor could be found starting from the context that was passed to BlocProvider.of<BlocA>().
    //...
    

    and this is because as I did said, when pushing a new route, a new subtree will be opened as a child of Navigator widget, from it's context, it will lookup for that BlocA, which will not find because on top of it, there is only a MaterialApp and none of the BlocA.

    Here, you will know the value of BlocProvider and how it will help you for such cases, by passing the BlocA to it when navigating to SubTreeTwo, your passing that bloc to that subtree manually and make it availabele there, which fixes the issue.