Hi I'm new to Flutter and Provider and can't make sense of this error. Basically I'm using MultiProvider to manage the states like below, and this works really great for one of them (Auth) but not other(User) even though I'm using them in the same way.
I get this error.
Note there are actually more providers, but simplified it for sake of simpler code example
Error
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following ProviderNotFoundException was thrown building StartPage(dirty, dependencies: [_InheritedProviderScope<Auth>, _InheritedProviderScope<UserLocation>, _InheritedProviderScope<UserState>, _InheritedProviderScope<BottomNavigationBarProvider>]):
Error: Could not find the correct Provider<User> above this StartPage Widget
To fix, please:
* Ensure the Provider<User> is an ancestor to this StartPage Widget
* Provide types to Provider<User>
* Provide types to Consumer<User>
* Provide types to Provider.of<User>()
* Ensure the correct `context` is being used.
If none of these solutions work, please file a bug at:
Main.dart
class SchoolApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Auth()),
ChangeNotifierProvider(create: (_) => User()),
],
child: HomePage(),
));
}
}
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
UserState _userState = Provider.of<UserState>(context);
switch (_userState.status) {
case UserStatus.UnAuthenticated:
return LoginScreen();
case UserStatus.Authenticated:
return StartPage();
}
}
}
StartPage.dart
class StartPage extends StatelessWidget with ChangeNotifier {
Timer timer;
#override
Widget build(BuildContext context) {
final _auth = Provider.of<Auth>(context);
final _user = Provider.of<User>(context, listen: true);
...
User.dart
class User extends ChangeNotifier{
Firestore db = Firestore.instance;
String _userId, _firstName, _lastName, _school, _email, _description, _employer, _title, _name;
List<String> _ideologies, _interests, _religions;
UserType _userType;
...
Auth.dart
class Auth extends ChangeNotifier {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
Future<String> signInWithEmailAndPassword(String email, String password) async {
final AuthResult authResult = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
notifyListeners();
return authResult.user.uid.toString();
}
...
class SchoolApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<Auth>(create: (_) => Auth()),
ChangeNotifierProvider<User>(create: (_) => User()),
],
child: MaterialApp(
title: 'Flutter Demo',
home: HomePage(),
));
}
}
Wrap your MaterialApp with MultiProvider. I might have made some bracket issue in my code above
Try to wrap with Consumer the widget that requires to get date from provider:
Foo(
child: Consumer<Auth, User>(
builder: (context, auth, user, child) {
return YourWidget(a: yourProviderData, child: child);
},
child: Baz(),
),
)
You can try also wrap your code with the widget Builder(builder: (context) => ), to have access to the nearest context reference.
Answering my question for visibility. So found the answer to my question here. https://github.com/rrousselGit/provider/issues/422#issuecomment-632030627
Basically I imported the package in the wrong way, I did
import 'path/file.dart'
Instead of specifying the full path
import 'package:/path/file.dart'
Related
I have learned that it brings problems when initating a future- or streambuilder inside the build of a widget since it will lead to unwanted fetching of data.
To do this you pass the stream outside of the build-method. But this leads to another problem. In my case I have to get an id-argument from the last widget but that can only be reached inside the build. I have searched the internet for explanations on how to solve this but I cant find an easy and clean explanation on the best way to do this.
A good solution that initialize the future with the build outside of the build.
My material app:
MaterialApp(
routes: appRoutes,
);
My routes table:
var appRoutes = {
Screen1.routeName: (context) => Screen1(),
Screen2.routeName: (context) => Screen2(),
};
How I send the argument from screen1:
onTap: () {
Navigator.of(context).pushNamed(
Screen2.routeName,
arguments: id,
);
},
screen2: (which doesnt work this way)
class Screen2 extends StatefulWidget {
static const routeName = '/screen2';
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
#override
final String id = ModalRoute.of(context)!.settings.arguments as String; <====== Impossible
final _stream = FirestoreService().getData(id);
Widget build(BuildContext context) {
return FutureBuilder<Map>(
future: _stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
print('succes');
} else {
print('Fail');
}
});
}
}
Way1: late keyword (unstable)
Use late keyword for lazy initialiation of your argument, stream.
class Screen2 extends StatefulWidget {
static const routeName = '/screen2';
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
#override
late final String id = ModalRoute.of(context)!.settings.arguments as String; <====== Impossible
late final _stream = FirestoreService().getData(id);
Widget build(BuildContext context) {
...
}
}
Way2: onGerateRoute + constructor (good)
You can check this answer for using onGenerateRoute. It is more proper way.
Way3: Create extraction delegation screen (good)
In the docs, the best practice is creating a delegation screen for extract arguments.
It parse argument in build method and pass it to new screen.
// A Widget that extracts the necessary arguments from
// the ModalRoute.
class ExtractArgumentsScreen extends StatelessWidget {
const ExtractArgumentsScreen({super.key});
static const routeName = '/extractArguments';
#override
Widget build(BuildContext context) {
// Extract the arguments from the current ModalRoute
// settings and cast them as ScreenArguments.
final args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;
return Scaffold(
appBar: AppBar(
title: Text(args.title),
),
body: Center(
child: Text(args.message),
),
);
}
}
I'm getting a providerNotFoundException, and I suspect that there is a context mismatch in the below code, but I'm having a hard time seeing it. As I understand, problems with BuilderContext arise when an .of method is performed within the same build method, but I don't see this happening in this case. Some of the Provider.of methods work fine as annotated in the below code, but as soon as SchedulingPage is called, the Provider.of methods no longer work.
What is the problem here?
Edit: I updated to use my full code below:
Here's the full error: ProviderNotFoundException (Error: Could not find the correct Provider above this LoginForm Widget
void main() {
runApp(
Home(),
);
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// ChangeNotifierProvider(create: (context) => CalendarData()),
ChangeNotifierProvider(create: (context) => LayoutData()),
],
child: MyApp(),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
print("constraints: $constraints");
Size mediaSize = MediaQuery.of(context).size;
double safeAreaSize = mediaSize.height - constraints.maxHeight;
Provider.of<LayoutData>(context).safeAreaDiff = safeAreaSize;
Provider.of<LayoutData>(context).safeArea = constraints;
Provider.of<LayoutData>(context).mediaArea = mediaSize;
var test = Provider.of<LayoutData>(context).mediaArea.width;
print(test); // this works
return Scaffold(body: LoginScreen());
}),
),
);
}
}
class LoginScreen extends StatelessWidget {
const LoginScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
double test = Provider.of<LayoutData>(context).mediaArea.width;
print("test: $test"); // this works
return LoginForm();
}
}
class LoginForm extends StatefulWidget {
LoginForm({Key key}) : super(key: key);
#override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
#override
Widget build(BuildContext context) {
double width = Provider.of<LayoutData>(context).mediaArea.width; // The code fails here
print('width: $width');
return Text("this is where it fails ^^^^^^^^");
}
}
class LayoutData with ChangeNotifier {
double safeAreaDiff = 0.0;
BoxConstraints safeArea;
Size mediaArea;
LayoutData() {
initializeApp();
}
void initializeApp() {
print("layout initialized");
}
}
You can not access provider in same class in which you create. That must be parent widget.
void main() {
runApp(Home());
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CalendarData()),
ChangeNotifierProvider(create: (context) => LayoutData()),
],
child: MyApp(),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
print("constraints: $constraints");
Size mediaSize = MediaQuery.of(context).size;
double safeAreaSize =
mediaSize.height - constraints.maxHeight; // works
Provider.of<LayoutData>(context).safeAreaDiff =
safeAreaSize; // works
Provider.of<LayoutData>(context).safeArea = constraints; // works
Provider.of<LayoutData>(context).mediaArea = mediaSize; // works
Provider.of<CalendarData>(context).working = "beer"; // works
print(Provider.of<CalendarData>(context).working); // works
return Scaffold(body: SchedulingPage());
}),
),
);
}
}
Output:
Performing hot restart...
Restarted application in 1,181ms.
I/flutter (25187): constraints: BoxConstraints(w=411.4, h=659.4)
I/flutter (25187): layout initialized
I/flutter (25187): 411.42857142857144
I/flutter (25187): test: 411.42857142857144
I/flutter (25187): width: 411.42857142857144
I'm posting this on the off chance that someone has the same problem that I did. It turned out the issue was not fixed by the chosen solution. The code, as written should have worked. However, in this case, the solution was difficult to find. It turned out that the import itself was incorrect. There were two imports of the Provider Data Class, like this:
import 'package:myProject/providers/CalendarData.dart';
and
import 'package:gcfdlayout2/Providers/CalendarData.dart';
This ambiguity confused the IDE, I believe, and, although it didn't give me any errors at build time, it did at run time, but the "could not find Provider" made me assume that it couldn't find the Provider in the Tree, not in the code itself.
The way I finally found this was to use other methods of running the code. Initially, I was only using Visual Studio Code, but I never got any errors that suggested it was an import problem. I changed to Android Studio, and it informed me that there were two imports for the CalendarData class.
Have you tried placing your MultiProvider() above your MaterialApp()?
In my app, I have a model that store the user logged in my app.
class AuthenticationModel extends ChangeNotifier {
User _user;
User get user => _user;
void authenticate(LoginData loginData) async {
// _user = // get user from http call
notifyListeners();
}
void restoreUser() async {
//_user = // get user from shared prefs
notifyListeners();
}
}
The model is registered at the top of the widget tree :
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthenticationModel(),
child: MaterialApp(
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => PrehomeScreen(),
'/home': (context) => HomeScreen()
},
),
);
}
}
Somewhere down the widget tree, I have a button that calls the Model :
child: Consumer<AuthenticationModel>(
builder: (context, authModel, child) {
return MyCustomButton(
text: 'Connect',
onPressed: () {
authModel.authenticate(...)
},
);
},
),
Now, I would like, somewhere, listen to the changes on the AuthenticationModel to trigger a Navigator.pushReplacmentNamed('/home') when the user is not null in the model.
I tried to do it in the builder of Prehome :
class PrehomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthenticationModel>(
builder: (context, authModel, child) {
if (authModel.user != null) {
Navigator.of(context).pushReplacementNamed("/home")
}
return Container(
child: // Prehome UI
);
},
);
}
}
but I have a error when doing it like this :
════════ (2) Exception caught by widgets library ═══════════════════════════════════════════════════
setState() or markNeedsBuild() called during build.
The relevant error-causing widget was:
Consumer<AuthenticationModel> file:///Users/pierre.degand/Projects/cdc/course_du_coeur/lib/Prehome.dart:13:12
═══════════════════════════════════════════════════════════════════════════════
How can I setup such a listener ? Is it a good practice to trigger navigation on model changes like this ?
Thanks
EDIT: I found a way to make this work. Instead of using Consumer inside the PrehomeScreen builder, I used the following code :
class PrehomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
Provider.of<AuthenticationModel>(context).addListener(() {
Navigator.of(context).pushReplacementNamed("/home");
});
return Container(
child: // UI
);
}
}
It works fine, the navigation is executed when the model changes. But there is an error message in the console (printed 3 times) :
════════ (4) Exception caught by foundation library ════════════════════════════════════════════════
Looking up a deactivated widget's ancestor is unsafe.
════════════════════════════════════════════════════════════════════════════════════════════════════
The app does not crash so, for now, I'm ok with this.
I still want to know if this is a good approach or not.
I prefer to use Stream or rxdart PublishSubject BehaviourSubject for listening to any activity or to manage global app data.
I implement it using bloc pattern. Basically bloc pattern is just like redux for react means creating a central dataset that contains all app data and you don't have to do prop drilling.
You can create Stream like this.
import 'package:rxdart/rxdart.dart';
class AbcBloc {
BehaviorSubject<bool> _connectivity;
AbcBloc() {
_connectivity = BehaviorSubject<bool>();
}
// stream
Stream<bool> get connectivity => _connectivity.stream;
// sink
Function(bool) get updateConnectivity => _connectivity.sink.add;
dispose(){
_connectivity.close();
}
}
void createAbcBloc() {
if (abcBloc != null) {
abcBloc.dispose();
}
abcBloc = AbcBloc();
}
AbcBloc abcBloc = AbcBloc();
now you can access that abcBloc variable from anywhere and listen to connectivity variable like this
import './abcBloc.dart';
void listenConnectivity(){
abcBloc.connectivity.listen((bool connectivety){
here you can perform your operations
});
}
and you can update connectivity from abcBloc.updateConnectivity(false);
every time you perform any changes that listener will get called.
remember you have to call listenConnectivity() one time to get it activated;
void main() {
Provider.debugCheckInvalidValueType = null;
return runApp(
Provider(
create: (_) => AuthenticationModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final navigatorKey = GlobalKey<NavigatorState>();
Provider.of<AuthenticationModel>(context).addListener(() {
final authModel = Provider.of<AuthenticationModel>(context);
if (authModel.user != null) {
navigatorKey.currentState.pushReplacementNamed("/home");
}
});
return MaterialApp(
navigatorKey: navigatorKey,
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => PrehomeScreen(),
'/home': (context) => HomeScreen()
},
);
}
}
I don't think ChangeNotifier is needed.
void main() async {
final isLoggedIn = await Future.value(true); // get value from shared prefs or your model
runApp(MyApp(isLoggedIn));
}
class MyApp extends StatelessWidget {
MyApp(this.isLoggedIn);
final bool isLoggedIn;
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: isLoggedIn ? '/home' : '/',
routes: {
'/': (context) => HomeScreen(),
'/login': (context) => LoginScreen()
},
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Logout'),
onPressed: () => Navigator.of(context).pushReplacementNamed("/login"),
);
}
}
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Login'),
onPressed: () => Navigator.of(context).pushReplacementNamed("/"),
);
}
}
I'm still relatively new to flutter and even newer to Provider so I may be entirely off with this but from what I've read it looks correct.
General idea is there's a header widget with a button that will either open an endrawer or bring the user to a login page depending on the state of the app.
Login works and the states all are working correctly but only on the login widget. When the user is routed back to the main screen - the state is still in its default state even though the state gets set on a successful login.
The widget tree is like so:
Main
|_ HomeScreen
| |_ AppHeader
|_ Login
main.dart
Widget build(BuildContext context) {
return MultiProvider (
providers: [
ChangeNotifierProvider (create: (_) => LoginState(),)
],
child: MaterialApp(
title: kAppTitle,
theme: alcDefaultLightTheme(),
home: HomeScreen(title: "kAppTitle"),
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
const AlcLocalizationsDelegate(),
],
supportedLocales: [
const Locale(kEn),
const Locale(kFr),
],
initialRoute: HomeScreen.id,
routes: {
LoadingScreen.id: (context) => LoadingScreen(),
HomeScreen.id: (context) => HomeScreen(title: kAppTitle),
}),
);
}
home_screen.dart
class HomeScreen extends StatefulWidget {
static const String id = 'home_screen';
HomeScreen({Key key, this.title}) : super(key: key);
final String title;
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
AccountDrawerOpen() {
_scaffoldKey.currentState.openEndDrawer();
FirebaseAnalytics().logEvent(
name: 'account_drawer_open',
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
...display code here
body: AppHeader()
);}
}
And this is where I need to access the state to determine if the player is logged in or not
app_header.dart
import 'package:provider/provider.dart';
class AppHeader extends StatelessWidget {
#override
Widget build(BuildContext context) {
LoginState testLoginState = Provider.of<LoginState>(context);
return Column(
children: <Widget>[
FlatButton(
child: Text('Check state'),
onPressed: () {
print("APP HEADER | STATE IS NOW ${testLoginState.status}");
},
)
],
);
}
}
Lastly, here's my LoginState.dart
enum Status {
Authenticated,
Authenticating,
Unauthenticated,
InvalidLogin
}
class LoginState with ChangeNotifier {
Status _status = Status.Unauthenticated;
Status get status => _status;
Future signIn(String email, String password) async {
try {
_status = Status.Authenticating;
notifyListeners();
... goes to the DB, some logic happens and returns true
_status = Status.Authenticated;
notifyListeners();
print("FROM LOGIN STATE: $_status");
} catch (e) {
print('Oops');
_status = Status.InvalidLogin;
notifyListeners();
}
}
Any help is appreciated, thanks for your help.
Figured it out. In my Login widget - I had a ChangeNotifierProvider which changes the context. So in this case - this changed the context to the lowest possible widget - the login widget.
I have two streams:
Stream<FirebaseUser> FirebaseAuth.instance.onAuthStateChanged
Stream<User> userService.streamUser(String uid)
My userService requires the uid of the authenticated FirebaseUser as a parameter.
Since I will probably need to access the streamUser() stream in multiple parts of my app, I would like it to be a provider at the root of my project.
This is what my main.dart looks like:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
var auth = FirebaseAuth.instance;
var userService = new UserService();
return MultiProvider(
providers: [
Provider<UserService>.value(
value: userService,
),
],
child: MaterialApp(
home: StreamBuilder<FirebaseUser>(
stream: auth.onAuthStateChanged,
builder: (context, snapshot) {
if (!snapshot.hasData) return LoginPage();
return StreamProvider<User>.value(
value: userService.streamUser(snapshot.data.uid),
child: HomePage(),
);
}),
),
);
}
}
The issue is that when I navigate to a different page, everything below the MaterialApp is changed out and I lose the context with the StreamProvider.
Is there a way to add the StreamProvider to the MultiProvider providers-list?
Because when I try, I also have to create another onAuthStateChanged stream for the FirebaseUser and I don't know how to combine them into one Provider.
So this seems to work fine:
StreamProvider<User>.value(
value: auth.onAuthStateChanged.transform(
FlatMapStreamTransformer<FirebaseUser, User>(
(firebaseUser) => userService.streamUser(firebaseUser.uid),
),
),
),
If anybody has doubts about this in certain edge cases, please let me know.
Thanks to pskink for the hint about flatMap.
Maybe you can try this approach:
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<FirebaseUser>(
builder: (_) => FirebaseUser(),
),
],
child: AuthWidgetBuilder(builder: (context, userSnapshot) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.indigo),
home: AuthWidget(userSnapshot: userSnapshot),
);
}),
);
}
}
AuthWidgetBuilder.dart
Used to create user-dependant objects that need to be accessible by
all widgets. This widget should live above the [MaterialApp]. See
[AuthWidget], a descendant widget that consumes the snapshot generated
by this builder.
class AuthWidgetBuilder extends StatelessWidget {
const AuthWidgetBuilder({Key key, #required this.builder}) : super(key: key);
final Widget Function(BuildContext, AsyncSnapshot<User>) builder;
#override
Widget build(BuildContext context) {
final authService =
Provider.of<FirebaseUser>(context, listen: false);
return StreamBuilder<User>(
stream: authService.onAuthStateChanged,
builder: (context, snapshot) {
final User user = snapshot.data;
if (user != null) {
return MultiProvider(
providers: [
Provider<User>.value(value: user),
Provider<UserService>(
builder: (_) => UserService(uid: user.uid),
),
],
child: builder(context, snapshot),
);
}
return builder(context, snapshot);
},
);
}
}
AuthWidget.dart
Builds the signed-in or non signed-in UI, depending on the user
snapshot. This widget should be below the [MaterialApp]. An
[AuthWidgetBuilder] ancestor is required for this widget to work.
class AuthWidget extends StatelessWidget {
const AuthWidget({Key key, #required this.userSnapshot}) : super(key: key);
final AsyncSnapshot<User> userSnapshot;
#override
Widget build(BuildContext context) {
if (userSnapshot.connectionState == ConnectionState.active) {
return userSnapshot.hasData ? HomePage() : SignInPage();
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
This is originally from the tutorial of advance provider from Andrea Bizotto.
But I tailored some the code according to your your code above.
Hope this works, good luck!
Reference:
https://www.youtube.com/watch?v=B0QX2woHxaU&list=PLNnAcB93JKV-IarNvMKJv85nmr5nyZis8&index=5