Flutter BLoC Provider Query - flutter

I've recently started playing with BLoC pattern in Flutter and am struggling to understand an issue with the BLoC provider. My class looks like this
class LoginBlocProvider extends InheritedWidget {
final LoginBloc bloc;
LoginBlocProvider({Key key, Widget child})
: bloc = LoginBloc(),
super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
static LoginBloc of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<LoginBlocProvider>().bloc;
}
}
Now most of the articles I've read say to add the Provider to the widget tree right above the Material app
return LoginBlocProvider(
child: MaterialApp(...)
)
My issue with this is what happens if you have a complex app with a large number of screens. It seems this would get messy really quickly
return LoginBlocProvider(
child: AccountBlocProvider(
child: ScreenOne(
child: ScreenTwo(
child: ScreenThree(
...
)
)
)
)
)
Is there more efficient way to manage this?

This page explains how to get around the readability issue of providing all of your blocs at the start of the application. There is a Widget called MultiBlocProvider that takes a list of Provider widgets.
So it would look like this:
return MultiBlocProvider(
providers: [
BlocProvider<LoginBloc>(
create: (BuildContext context) => LoginBloc(),
),
BlocProvider<AccountBloc>(
create: (BuildContext context) => AccountBloc(),
),
BlocProvider<PageOneBloc>(
create: (BuildContext context) => PageOneBloc(),
),
],
child: MaterialApp(...)
)

Related

Consumer and context.watch in MultiProvider

I am trying Flutter for the first time, and I am a little confused by the MultiProvider class.
The question is straightforward, but I didn't find an explanation:
when should one use Consumer and when context.watch?
For instance, taking one of the examples apps I have found, I tried using two providers for two global states, the theme and the status of the app:
runApp(
MultiProvider(providers: [
ChangeNotifierProvider(create: (context) => AppTheme()),
ChangeNotifierProvider(create: (context) => AppStatus()),
],
child: const MyApp()
));
Then the app widget accesses the theme with Consumer:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<AppTheme>(
builder: (context, appTheme, child) {
// ...
As far as I understand, now all children widgets will inherit the provider. Is it right?
My home page, then, called by the MyApp class does not use Consumer, but context.watch:
#override
Widget build(BuildContext context) {
final appTheme = context.watch<AppTheme>();
final appStatus = context.watch<AppStatus>();
return NavigationView(
// ...
It works, don't get me wrong, but I just copied the row above my appStatus, so I don't really fully understand it. This is also due to another screen that I've concocted to access the AppStatus global state, but I use Consumer, as suggested by the Flutter documentation:
class _ViewerState extends State<Viewer> {
#override
Widget build(BuildContext context) {
return Consumer<AppStatus>(
builder: (context, appStatus, child) {
return ScaffoldPage.scrollable(
header: const PageHeader(title: Text('Test')),
children: [
FilledButton(child: Text("Try ${appStatus.count}"), onPressed: (){ appStatus.increment(); debugPrint('pressed ${appStatus.count}'); }),
FilledButton(child: Text("Reset"), onPressed: (){ appStatus.reset(); }),
]);
},
);
}
}
I have the feeling that I am misusing something here, and I do not really understand what's going on under the hood...
context.watch<T>() and Consumer<T> does the same thing. Most of the time context.watch<T>() is just more convenient. In some cases where context is not available Consumer<T> is useful.

Is this the correct way to use Bloc in flutter?

How to use Bloc in flutter. What is the best way to use it? to wrap the whole app with blocprovider?
runApp(
RepositoryProvider(
create: (context) => API(),
child: MultiBlocProvider(
providers: [
BlocProvider<GlobalViewBloc>(
lazy: false,
create: (BuildContext context) =>
GlobalViewBloc(context.read<API>()),
),
BlocProvider<CountryDetailViewBloc>(
lazy: false,
create: (BuildContext context) =>
CountryDetailViewBloc(context.read<API>()),
),
],
child: MaterialApp(
home:MyApp(),
),
),
));
there are two ways of accessing bloc:
1 - global declaration
you declare a bloc variable inside your bloc and all widgets and whole app will have access to that variable. like this:
final bloc = YourBloc();
2 - using provider
in this declaration you have to define the provider in the highest widget which you want having the access to bloc and all of its children will have access to that bloc:
class WD extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = YourBlocProvider.of(context);
/....
);
}
}

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

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.
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());
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.
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.
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(),
),
}
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
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

Repository provider in the flutter_bloc library doesn't provide repository with when pushing new route

I am using the flutter_bloc library to architect my app. In addition to the BlocProvider I am using the Repository Provider, since I will be using a specific repository extensively throughout my app. But I am having an issue with regards to context . Below is snippets of my code:
main.dart
void main() async {
.......
appRepository _appRepository = AppRepository();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((_) {
runApp(
BlocProvider(
builder: (context) =>
AuthenticationBloc(appRepository: _appRepository)..dispatch(AppStarted()),
child: App(appRepository: _appRepository,),
),
);
});
}
class App extends StatelessWidget {
............
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (BuildContext context, AuthenticationState state) {
.....
if (state is AuthenticationUnauthenticated) {
return SafeArea(
top: false,
bottom: false,
child: RepositoryProvider(
builder: (context) => _appRepository,
child: LoginPage(firebaseMessaging: _firebaseMessaging),
),
);
}
......
},
),
);
}
}
Register button found in login form:
register_button.dart
class RegisterButton extends StatelessWidget {
final FirebaseMessaging _firebaseMessaging;
RegisterButton({
Key key,
#required FirebaseMessaging firebaseMessaging,
}) : assert(firebaseMessaging != null),
_firebaseMessaging = firebaseMessaging,
super(key: key);
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Don't have an account?", style: TextStyle(color: Colors.black)),
SizedBox(width: 4.0),
GestureDetector(
child: Text("Register here!",
style: TextStyle(
color: Color(0xFF585B8D), fontWeight: FontWeight.w500)),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return RegisterPage(
firebaseMessaging: _firebaseMessaging,
);
}),
);
},
)
],
);
}
register_page.dart
class RegisterPage extends StatelessWidget {
final FirebaseMessaging _firebaseMessaging;
RegisterPage({
Key key,
#required FirebaseMessaging firebaseMessaging,
}) : assert(firebaseMessaging != null),
_firebaseMessaging = firebaseMessaging,
super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider(
builder: (context) => RegisterBloc(
appRepository: RepositoryProvider.of<AppRepository>(context),
firebaseMessaging: _firebaseMessaging,
),
child: RegisterForm(),
),
);
}
}
Question:
I'm getting an error when I click on the register button on my login form that says the following:
No ancestor could be found starting from the context that was passed to RepositoryProvider.of<AppRepository>().
This can happen if:
1. The context you used comes from a widget above the RepositoryProvider.
2. You used MultiRepositoryProvider and didn't explicity provide the RepositoryProvider types.
Good: RepositoryProvider<AppRepository>(builder: (context) => AppRepository())
Bad: RepositoryProvider(builder: (context) => AppRepository()).
The context used was: BlocProvider<RegisterBloc>(dirty, state: _DelegateWidgetState#a87b2(lifecycle state: created))
Why am I getting this error? This problem seems to be fixed if I put the repository provider as the child of the blocprovider and app as the child repository provider in the main function and then deleting the invidual repository providers in App(). I'm guessing the issue is from pushing the material page route from the button. I don't think I understand how context or provider exactly works in Flutter. I thought the provider would look up the widget tree for the repository/bloc, does pushing a route some how break this continuity?
When you use Navigator.of(context).push or Navigator.of(context).pushNamed the widget pushed is not a child of the widget that call Navigator.of(context).push or Navigator.of(context).pushNamed, this widget is a child of the closest instance of Navigator that encloses the given context, in your case the Navigator is created by the MaterialApp, so if you want to provide the Repository or Bloc to different routes, the Provider must be a parent of the Navigator, in your case must be a parent of MaterialApp.

How to share the bloc between contexts

I'm trying to access the bloc instance created near the root of my application after navigating to a new context with showDialog(). However, if I try getting the bloc like I usually do, by getting it from the context like _thisBlocInstance = BlocProvider.of<ThisBlocType>(context), I get an error that indicates there is no bloc provided in this context.
I assume this is because the showDialog() builder method assigns a new context to the widgets in the dialog that don't know about the Bloc I am trying to find, which was instantiated as soon as the user logs in:
#override
Widget build(BuildContext context) {
_authBloc = BlocProvider.of<AuthBloc>(context);
_accountBloc = AccountBloc(authBloc: _authBloc);
return BlocProvider(
bloc: _accountBloc,
....
There is a button in the corner that displays a dialog:
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => EventDialog(),
).then(
(val) => print(val)
),
child: Icon(Icons.add),
),
),
);
}
And in the EventDialog, I try to find the bloc with the context again:
#override
void build(BuildContext context) {
_accountBloc = BlocProvider.of<AccountBloc>(context);
_userMenuItems = _accountBloc.usersInAccount
.map((user) => DropdownMenuItem(
child: Text(user.userName),
value: user.userId,
))
.toList();
}
And this fails, with an error 'the getter bloc was called on null', or, there is no bloc of that type attached to this context.
Is there some way to access the bloc just from the context after using showDialog(), or otherwise navigating to a new context?
This is the bloc provider class:
import 'package:flutter/material.dart';
//This class is a generic bloc provider from https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/
//it allows easy access to the blocs by ancestor widgets and handles calling their dispose method
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
#override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
#override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context){
return widget.child;
}
}
abstract class BlocBase {
void dispose();
}
The best way I found to access the original bloc in a new context is by passing a reference to it to a new bloc that manages the logic of the new context. In order to keep the code modular, each bloc shouldn't control more than one page worth of logic, or one thing (e.g. log-in state of the user). So, when I create a new screen/context with showDialog(), I should also have a new bloc that deals with the logic in that screen. If I need a reference to the original bloc, I can pass it to the constructor of the new bloc via the dialog widget's constructor, so any information in the original bloc can still be accessed by the new bloc/context:
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => NewEventDialog(
accountBloc: BlocProvider.of<AccountBloc>(context),
),
).then((event) => eventsBloc.addEvent(event)),
...
class NewEventDialog extends StatelessWidget {
final AccountBloc accountBloc;
NewEventBloc _newEventBloc;
NewEventDialog({this.accountBloc}) : assert(accountBloc != null);
#override
Widget build(BuildContext context) {
_newEventBloc = NewEventBloc(accountBloc: accountBloc);
return BlocProvider(
bloc: _newEventBloc,
...
The last answer is okay but it can be simplified, that is just transfering Bloc to its child widget.
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => EventDialog((
accountBloc: BlocProvider.of<AccountBloc>(context),
),
).then(
(val) => print(val)
),
child: Icon(Icons.add),
),
),
);
}
class NewEventDialog extends StatelessWidget {
final AccountBloc accountBloc;
NewEventDialog({this.accountBloc}) : assert(accountBloc != null);
#override
void build(BuildContext context) {
_accountBloc = accountBloc;
_userMenuItems = _accountBloc.usersInAccount
.map((user) => DropdownMenuItem(
child: Text(user.userName),
value: user.userId,
))
.toList();
}
So far I find this problem occurs when going to widget via page routing. We can transfer the Bloc widget to widget to avoid this problem.