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.
Related
I've been working with Flutter recently, and I saw that there was many ways to deal with state management.
Following the recommendations there, I've been using Provider to deal with the state of my app.
I can update a part of my state from one of the widgets in my UI. To do that, I can call a method of the provider that's above the current widget in the context. No problems with this.
But I want the update of my state to be made from an overlay.
The issue is: When I'm inserting an OverlayEntry with Overlay.of(context)?.insert(), it inserts the overlayEntry to the closest Overlay, which is in general the root of the app, which is above the ChangeProvider. As a result, I get an exception saying I can't find the Provider from the OverlayEntry.
Here is a replication code I've been writting:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider(
create: (context) => NumberModel(), // All widgets that will be lower in the widget tree will have access to NumberModel
child: NumberDisplayer()
),
);
}
}
// Simple ChangeNotifier. We have a number that we can increment.
class NumberModel extends ChangeNotifier {
int _number = 10;
int get number => _number;
void add_one() {
_number = number + 1;
notifyListeners();
}
}
// This class displays a number, and a button.
class NumberDisplayer extends StatelessWidget {
NumberDisplayer({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var overlayEntry = OverlayEntry(builder: (context) =>
Positioned(
top: 100,
left: 50,
child: FloatingActionButton(onPressed: (){
// Throws "Error: Could not find the correct Provider<NumberModel> above this _OverlayEntryWidget Widget"
Provider.of<NumberModel>(context, listen: false).add_one();
})));
return Consumer<NumberModel>(
builder: (context, numberModel, child) {
return Column(
children: [
Text('Number: ${numberModel.number}'),
FloatingActionButton(onPressed: () {
Overlay.of(context)?.insert(overlayEntry);
})
],
);
},
);
}
}
I would like to find a way to update the information in my provider from the overlay, but I'm not sure how to approach this problem.
Thanks for your help everyone !
I am confused about Provider. I think Provider is meant to encapsulate the state of a Widget so it can be accessed somewhere else throughout the program. The problem is: What if I want a certain stateless widget multiple times? I created an example for this:
Lets say we want to model a few pieces of paper. Each piece of paper has some unique writing on it. I could now make a provider for a single piece of paper like this:
class PaperSheetProvider extends ChangeNotifier {
String uniqueText = "";
void setUniqueText(String newText) {
uniqueText = newText;
notifyListeners();
}
}
and I make a simple paper widget to consume that provider like:
class PaperPieceWidget extends StatelessWidget {
const PaperPieceWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<PaperSheetProvider>(
builder: ((context, value, child) => Text(value.uniqueText)),
);
}
}
and at last, I make 2 paper widgets along with a button to change the text of the paper:
Column(
children: [
PaperPieceWidget(),
PaperPieceWidget(),
OutlinedButton(
onPressed: () {
Provider.of<PaperSheetProvider>(context, listen: false).setUniqueText('blablaablaa');
},
child: Text("change paper contents"))
],
),
(The ChangeNotifierProvider is near the root of the whole widget tree to simplify the code a bit)
Simple enough. But now If I click the button, I get:
Basically, the two paper pieces have the same writing. Which should not be the case, each piece of paper should have their own, unique writing. How do I do this correctly?
Full code in case anything is unclear:
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String title = 'Shortcuts and Actions Demo';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Column(
children: [
PaperPieceWidget(),
PaperPieceWidget(),
OutlinedButton(
onPressed: () {
Provider.of<PaperSheetProvider>(context, listen: false)
.setUniqueText('blablaablaa');
},
child: Text("change paper contents"))
],
),
);
}
}
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider(create: ((context) => PaperSheetProvider()))
], child: const MyApp()));
}
class PaperPieceWidget extends StatelessWidget {
const PaperPieceWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<PaperSheetProvider>(
builder: ((context, value, child) => Text(value.uniqueText)),
);
}
}
//(provider is posted entirely above)
Wrap each widget with own provider. Something like this:
PaperSheetProvider provider1;
ChangeNotifierProvider.value(
value: provider1,
child: PaperPieceWidget(),
)
Im using Flutter and flutter_bloc for a small app, and i used MultiBlocProvider to use multiple BlocProviders that i need in the main home page, and under the home page, there is a MainWidget, which can access the given Bloc easily by: BlocProvider.of<OldBloc>(context)
The MainWidget calls NewWidget as a dialog by: showDialog(context: context, builder: (context) => NewWidget())
The problem is, i cannot access OldBloc from NewWidget(), so i assumed that MainWidget isnt passing its context to NewWidget when using showDialog ?
HomeScreen.dart
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => OldBloc()..add(Initialize()),
),
BlocProvider(
create: (context) => OtherBloc()..add(Initialize()),
),
],
child: Stack(
children: [
MainWidget(),
MenuWidget(),
],
),
));
}
}
MainWidget.dart
import 'package:flutter/material.dart';
class MainWidget extends StatelessWidget {
const MainWidget({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return TextField(onTap: () => showDialog(context: context, builder: (context) => NewWidget()));
}
}
NewWidget.dart
import 'package:flutter/material.dart';
class NewWidget extends StatelessWidget {
const NewWidget({Key key}) : super(key: key);
#override
Widget build(context) {
return Text(BlocProvider.of<OldBloc>(context).name); // <------- THIS GIVES AN ERROR THAT IT CANT FIND A BLOC OF THE TYPE OldBloc
}
}
You can simply use this (suggested by Flex Angelov):
showDialog(
context: superContext,
builder: (_) {
return BlocProvider.value(
value: superContext.read<MyBloc>(),
child: const MyDialogWidget(),
);
},
);
and make sure superContext has access to your BLoC.
You have no access to BuildContext in your showDialog method, documentation:
The widget returned by the builder does not share a context with the location that showDialog is originally called from.
The context argument is used to look up the Navigator and Theme for the dialog. It is only used when the method is called.
Its also recommended to use a StatefulBuilder or a custom StatefulWidget, here is an example.
I solved this problem.
I use cubit instead of bloc but it doesn't matter.
ShowDialog doesn't pass parent context. So you should create a new cubit and pass your parent cubit and state in arguments for a new cubit. I called main cubit FooCubit, new cubit FooCubitWrapper and DialogWidget is child widget that needs some BLOC logic in showDialog.
var fooCubit = context.read<FooCubit>(); // your parrent cubit and state
return BlocBuilder<FooCubit , FooState>(
builder: (_, state) {
.
.///some logic///
.
showDialog(
context: context,
builder: (_) => // here you have to create new cubit
BlocProvider( // because there is no access to parent cubit
create: (_) => FooCubitWrapper(cubit, state),
child: DialogWidget(),
),
);
This is FooCubitWrapper. For example, we need the boo method from the parent cubit. So we need to create here boo method and inside we need to reference to parent boo method and emit parent state.
class FooCubitWrapper extends Cubit<FooState> {
FooCubit fooCubit;
FooCubitWrapper(this.fooCubit, FooState initialState) : super(initialState);
boo() {
fooCubit.boo();
emit(fooCubit.state);
}
}
And finally, in your DialogWidget, you do all like usual
var cubit = context.read<TagsCubitWrapper>();
return BlocBuilder<TagsCubitWrapper, TagsState>(
builder: (context, state) {
// work with methods and fields as usual
cubit.boo();
if !(state.someField) {}
});
I am using Flutter_bloc package to work with bloc pattern in flutter, but i am wondering if it is a good practice to use a MultiBlocProvider inside main function and add all of my blocs in there like this:
void main()async{
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(Mafqood());
}
class Mafqood extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers : [
BlocProvider<AuthBloc>(
create: (context) => AuthBloc(AuthInitialState(), AuthRepository()),
),
BlocProvider<LoginBloc>(
create: (context) => LoginBloc(LoginInitialState(), AuthRepository()),
),
BlocProvider<ProfileBloc>(
create: (context) => ProfileBloc(ProfileInitialState(), AuthRepository()),
),
],
child: MaterialApp(
or it is better to add the bloc just where I need it? and why?
Thanks in advance.
You should use MultiBlocProvider inside the main function as you did. This is the best practice. And this is the goal of the providers.
Edit:
Now I realized that there is another answer here.
The main usage of MultiBlocProvider is using the bloc object in different places inside your application before you had to define which bloc depends on another bloc.
If you have an app that each screen use its own bloc, then you don't have a need for MultiBlocProvideras you can creat the bloc in the build function of the screen
class ParentScreen extends StatelessWidget {
const ParentScreen ({Key? key, this.data}) : super(key: key);
final data;
#override
Widget build(BuildContext context) {
return BlocProvider<MyBloc>(
create: (_) => MyBloc()),
child: MyScreenBody());
}
}
Class MyScreenBody extends StatefulWidget {
const MyScreenBody({Key? key}) : super(key: key);
#override
_MyScreenBodyState createState() => _MyScreenBodyState();
}
class _MyScreenBodyState extends State<MyScreenBody> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<MyBloc, MyState>(
builder: (context, state) {
return //... your code
}
)
);
}
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(...)
)