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),
),
],
)
],
),
);
},
),
);
}
}
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.
BlocProvider
of an example BlocA
.SubTreeOne
.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.