I'm learning about flutter's provider and I'm suffering from one error.
The following code works.
[code1]
class Model extends ChangeNotifier {
void save() {
print('save');
}
}
class Main extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Model(),
child: Scaffold(
appBar: AppBar(
title: Text('test'),
),
body: NewWidget(),
),
);
}
}
class NewWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text(
"test",
),
onPressed: () {
context.read<Model>().save();
},
);
}
}
But the code below does not work.
[code2]
class Model extends ChangeNotifier {
void save() {
print('save');
}
}
class Main extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Model(),
child: Scaffold(
appBar: AppBar(
title: Text('test'),
),
body: RaisedButton(
child: Text(
"test",
),
onPressed: () {
context.read<Model>().save();
},
),
),
);
}
}
With this code, the following error is output when the button is pressed.
Error: Could not find the correct Provider<Model> above this Main Widget
This likely happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that Main is under your MultiProvider/Provider<Model>.
This usually happen when you are creating a provider and trying to read it immediatly.
For example, instead of:
I don't separate widgets, I want to write one widget like code2.
Please let me know if there is any good way.
Thanks!
In your first example, NewWidget is build with a new BuildContext, that already have access to it's ancestor, so this Widget can see the provider that you have created there with: context.read<Model>().
But, on your second example, you are creating and using your provider all in the same Widget Main so everything on the same BuildContext, and when you run context.read<Model>() flutter will try to look up in your Widget tree to find the Model, but it won't find it because you have just created it. That's a scenario where you could make use, of the Builder Widget:
class Main extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Model(),
child: Scaffold(
appBar: AppBar(
title: Text('test'),
),
body: Builder(
// Here the magic happens
// this builder function will generate a new BuilContext for you
builder: (BuildContext newContext){
return RaisedButton(
child: Text(
"test",
),
onPressed: () {
newContext.read<Model>().save();
},
);
}
),
),
);
}
}
By using the Builder Widget, you have the ability to create a new BuildContext the can be used to retrieve information about the provider you have just created, that's because your Builder widget, is build after your ChangeNotifierProvider, and it is a child of it, so it can easily look up and find this information on it's parent.
Also pay attention on what errors tell you, flutter compiler is really smart with that kind of issues:
Make sure that Main is under your MultiProvider/Provider. This
usually happen when you are creating a provider and trying to read it
immediatly.
These lines tell you exactly what I explained before.
In addition to the above answer, you can also use the Consumer class of the Provider package.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
class Model extends ChangeNotifier {
void save() {
print('save');
}
}
class Main extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Model(),
child: Scaffold(
appBar: AppBar(
title: Text('test'),
),
body: Consumer<Model>(
builder: (context, model, _) {
return RaisedButton(
child: Text(
"test",
),
onPressed: () {
context.read<Model>().save();
},
);
},
),
),
);
}
}
Wrap MaterialApp from MultiProvider. and set the Provider Class(es) in value.
Ex:
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: Cart(),
),
],
child: MaterialApp(
title: 'Perfumino',
home: Home(),
),
);
Related
it has a simple solution but i =cant find it . I have tried some solutions on github and stack overflow but it didnt work.
lets take a look at my code :
here is my brewlist.dart :
class BrewList extends StatelessWidget {
#override
Widget build(BuildContext context) {
final brew = Provider.of<List<Brew>>(context);
brew.forEach((brew) {
print(brew.name);
print(brew.strength);
print(brew.sugars);
});
return Container();
}
}
its above widget is here in the home.dart file
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
final AuthService _authService = AuthService();
return StreamProvider<List<Brew>?>.value(
initialData: null,
value: DataBase('fjsbfjjhsbdfgvbdkbvkj').brews,
child: Scaffold(
backgroundColor: Colors.brown[50],
appBar: AppBar(
backgroundColor: Colors.brown[400],
elevation: 12,
title: Text('Brew Crew'),
actions: <Widget>[
TextButton.icon(
onPressed: () async {
await _authService.signOut();
},
icon: Icon(
Icons.person,
color: Colors.brown[800],
),
label: Text(
'log out',
style: TextStyle(color: Colors.brown[800]),
),
),
],
),
body: BrewList(),
),
);
}
}
brewlist is the body of my scaffold as you can see we have the StreamProvider wrapped around the scaffold to have the data passing through its childs {which brewlist is one of them ..
here is my main.dart file :
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<ToggleView>(create: (_) => ToggleView()),
ChangeNotifierProvider<LoadingIndicator>(
create: (_) => LoadingIndicator()),
],
child: MyApp(),
),
);
}
enter code here
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return StreamProvider<Userr?>.value(
value: AuthService().user,
initialData: Userr(useruid: 'No user found'),
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Wrapper(),
),
);
}
}
i really dont understand this long error :
The following ProviderNotFoundException was thrown building BrewList(dirty):
Error: Could not find the correct Provider<List> above this BrewList Widget
This happens because you used a BuildContext that does not include the provider
of your choice. There are a few common scenarios:
You added a new provider in your main.dart and performed a hot-reload.
To fix, perform a hot-restart.
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that BrewList is under your MultiProvider/Provider<List>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
Widget build(BuildContext context) {
return Provider(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because context is associated
// to the widget that is the parent of Provider<Example>
child: Text(context.watch()),
),
}
consider using builder like so:
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
If none of these solutions work, consider asking for help on StackOverflow:
i dont why I get this error if you can provide solution please help
i really appreciate your help inadvacne.
The solution is very simple, my friend.
Just go to brew_list.dart file and add the following line
final brews = Provider.of<List<Brew?>?>(context) ?? [];
That's it, you are good to go :)
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
I'm new to Flutter and provider package so any assistance would be great, so the issue I have my main.dart file which is as follows:
void main() => runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
home: ChangeNotifierProvider(
create: (_) => InterestingMomentProvider(),
child: Home(),
),
),
);
This builds my Home widget, I won't post it all as It's extremely large, however, what happens is I click a button and it passes in a string to the provider class an adds it to the list which is outlined as follows:
import 'package:flutter/widgets.dart';
class InterestingMomentProvider extends ChangeNotifier {
List<String> _moments = [];
List<String> get moments => _moments;
void addMoment(String time){
_moments.add(time);
}
int momentsTotal(){
return _moments.length;
}
}
Adding a breakpoint on the addMoment method I can confirm _moments has all the strings.
I then press a button which navigates to another screen, the navigation code is as follows:
Navigator.push(context, MaterialPageRoute(builder: (context) => MomentsRecorded()),);
MomentsRecorded widget is as follows:
class MomentsRecorded extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Moments'),
centerTitle: true,
),
body: Center(
child: MomentsList()
),
);
}
}
the first error was:
Could not find the correct Provider<InterestingMomentProvider> above this Consumer<InterestingMomentProvider> Widget
To fix, please:
* Ensure the Provider<InterestingMomentProvider> is an ancestor to this Consumer<InterestingMomentProvider> Widget
* Provide types to Provider<InterestingMomentProvider>
* Provide types to Consumer<InterestingMomentProvider>
* Provide types to Provider.of<InterestingMomentProvider>()
* Always use package imports. Ex: `import 'package:my_app/my_code.dart';
* Ensure the correct `context` is being used.
I then tweaked the body to look like the following and the error dissappeared:
body: Center(
child: ChangeNotifierProvider(
create: (_) => InterestingMomentProvider(),
child: MomentsList())
),
However inside MomentLists widget, I try to loop through the list of moments from the provider class, however when debugging _moments is 0 ?
MomentsList widget:
class MomentsList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<InterestingMomentProvider>(
builder: (context, momentData, child){
return momentData.momentsTotal() > 0 ? ListView.builder(
itemCount: momentData.momentsTotal(),
itemBuilder: (BuildContext context, int index) {
final moment = momentData.moments[index];
return ListTile(
title: Text(moment),
);
}
) : Center(child: Text('no moments recorded'),);
}
);
}
}
Can someone please explain why this maybe?
This is happening, because your provider is defined in home property of MaterialApp, so when you change the route the provider will be removed too.
Solution: move the provider above the MaterialApp like this:
void main() => runApp(
ChangeNotifierProvider(
create: (_) => InterestingMomentProvider(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
home: Home()
),
),
);
If you are afraid that this isn't right - checkout the docs, they are doing the same
I am using Provider for managing the state of my app. Here's how I am implementing it.
hypnose.dart
class _HypnoseAppState extends State<HypnoseApp> {
#override
Widget build(BuildContext context) {
UserService userService = UserService();
AudioUtilService audioUtilService = AudioUtilService();
return MultiProvider(
providers: [
ChangeNotifierProvider<UserService>.value(
value: userService,
),
ChangeNotifierProvider<AudioUtilService>.value(
value: audioUtilService,
)
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: Globals.title,
theme: ThemeData(primarySwatch: Colors.cyan),
darkTheme: ThemeData.dark(),
initialRoute: '/',
routes: {
'/': (BuildContext context) => WelcomeScreen(userService),
'/home': (BuildContext context) => HomePageSwitcher(),
'/audiocreate': (BuildContext context) => AudioCreateScreen()
}),
);
}
}
home_switcher.dart
class HomePageSwitcher extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<UserService>(
builder: (BuildContext context, UserService userService, Widget child) {
return Scaffold(
appBar: AppBar(),
drawer: Drawer(
child: Column(
children: <Widget>[
UserAccountsDrawerHeader(
accountEmail: Text(userService.loggedInUser.email),
accountName: Text(userService.loggedInUser.name),
currentAccountPicture:
Image.network(userService.loggedInUser.avatar),
)
],
),
),
body: Center(
child: RaisedButton(
child: Text('Sign out'),
onPressed: () async {
await userService.signOut();
Navigator.pushNamed(context, '/');
},
),
));
},
);
}
}
user_service.dart
class UserService extends ChangeNotifier {
// Get auth instances
final GoogleSignIn _googleSignIn = GoogleSignIn();
final FirebaseAuth _auth = FirebaseAuth.instance;
// Store reference of user collection
final CollectionReference userDb = Firestore.instance.collection('user');
// Master instance of logged in user
User _loggedInUser;
// Getter to access loggedInUser
User get loggedInUser {
return _loggedInUser;
}
PublishSubject<AuthState> _authStateSubject = PublishSubject();
.... other code
Now the problem here is that every time I hot reload, on the home page, I start to get the NoSuchMethodError as it says that properties like email, name etc. were called on null, which I think means that the state is lost. How can I overcome the same? Am I doing something wrong?
You should not use ChangeNotifierProvider.value. Instead use the default constructor:
ChangeNotifierProvider(
builder: (_) => UserService(),
)
Otherwise, your build method is impure and you'll have issues like described in How to deal with unwanted widget build?
The build method is designed in such a way that it should be pure/without side effects. This is because many external factors can trigger a new widget build, such as:
Route pop/push
Screen resize, usually due to keyboard appearance or orientation change
Parent widget recreated its child
An InheritedWidget the widget depends on (Class.of(context) pattern) change
This means that the build method should not trigger an http call or modify any state.
How is this related to the question?
The problem you are facing is that your build method has side-effects/is not pure, making extraneous build call troublesome.
Instead of preventing build call, you should make your build method pure, so that it can be called anytime without impact.
In the case of your example, you'd transform your widget into a StatefulWidget then extract that HTTP call to the initState of your State:
ChangeNotifierProvider(
create: (_) => UserService(),
),
Keep the key
class _HypnoseAppState extends State<HypnoseApp> {
Key key = UniqueKey();
...
}
and build:
return MultiProvider(
key: key, //<<<<<<<<<<<<<<<<<<<<<<Here
providers: ChangeNotifierProvider<UserService>.value(
value: userService,
),
ChangeNotifierProvider<AudioUtilService>.value(
value: audioUtilService,
)
],
I created a route for navigating from one page to another, in the following way
class task extends StatelessWidget{
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Task',
home: new task(),
routes: <String, WidgetBuilder>{
"/Completed": (BuildContext context) => new Completed()
}
);
}
}
class taskScreen extends StatefulWidget{
#override
taskState createState() => new taskState();
}
class taskState extends State<taskScreen> {
#override
Widget build(BuildContext context) {
Column taskScreen = Column(
children: <Widget>[
FlatButton(
..,
onPressed: (){
Navigator.of(context).pushNamed("/Completed");
},
child: Text(
"Completed",
),
),
],
)
]);
return Scaffold(
appBar: AppBar(
title: Text('Task Screen')),
body: taskScreen,
);
}
}
However when i try navigating it gives the error :
Could not find a generator for route RouteSettings("/Completed", null) in the _WidgetsAppState.
How can I fix this error?
I have used implemented route before from my main.dart page to the second page which worked properly however its not working here.
try this one
Navigator.pushNamed(context, "/Completed");
I figured out my error. Instead of instantiating task(),i had created an instance of taskScreen(). Hence the new route was never set. It solved my issue but I'm still curious as to what difference it made as both of them gave the same output Screen.