Here I am using provider in main.dart to stream "itemsInUserDocument" data from a document.
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<DocumentSnapshot>.value(
value: DatabaseService().itemsInUserDocument, <--- (1) Providing here
),
StreamProvider<User>.value(
value: AuthProvider().user,
),
],
My problem is passing the userUID to the Stream query here. My document id's on Firestore is my userUID.
final userUID = "3SlUigYBgsNgzjU8a9GSimhAhuu1"; <-- (2) Need to pass to stream "userID' below??
class DatabaseService {
Stream<DocumentSnapshot> get itemsInUserDocument {
final DocumentReference userData = Firestore.instance.collection('userdata').document(userUID);
return userData.snapshots();
}
}
Here I am using the list in down the widget tree.
#override
Widget build(BuildContext context) {
final userTaskDone = Provider.of<DocumentSnapshot>(context);
List taskList = []; <-- (3) Using the list here.
taskList = userTaskDone['tasks'].toList();
print(taskList);
It currently works and I am getting the streamed data in section (3) as is now, but I need to pass the Firebase user UID into the stream in section (2).
I can get the user UID in a stateful widget with setState function but everything I have tried doesn't work or seems like I am duplicating things too much, I am trying to use Provider to manage state properly.
I can get the userUID with provider as well, but you can only use it on a (context), where my notifier section (2) is only a class.
PLEASE help me with a solution.
EDIT:
I have tried the edits and the result is as follow:
Getting this critical error from provider when I enter the screen where it used to work before these changes to main.dart.
Here is the full code for main.dart with edits as it is now:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
title: 'Sendr',
home: MainScreen(),
routes: {
SplashPage.id: (context) => SplashPage(),
LoginScreen.id: (context) => LoginScreen(),
NavigatorScreen.id: (context) => NavigatorScreen(),
CragSelectionScreen.id: (context) => CragSelectionScreen(),
CragRoutesScreen.id: (context) => CragRoutesScreen(),
RouteDetailScreen.id: (context) => RouteDetailScreen(),
MapScreen.id: (context) => MapScreen(),
},
);
}
}
///Authentication Logic
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: AuthProvider().user,
child: Consumer<User>(
builder: (context, user, __) {
if (user == null) {
return LoginScreen();
} else {
return MultiProvider(
providers: [
Provider<DatabaseService>.value(
value: DatabaseService(user.uid),
),
],
child: NavigatorScreen(),
);
}
},
),
);
}
}
This is my Database Service:
import 'package:cloud_firestore/cloud_firestore.dart';
class DatabaseService {
DatabaseService(this.uid);
final String uid;
Stream<DocumentSnapshot> get itemsInUserDocument {
final DocumentReference userData =
Firestore.instance.collection('userdata').document(uid);
return userData.snapshots();
}
}
I have made no change to my step (3) where I am using it, is this where my problem might be now?
I am trying to do more research on this. Working through the provider docs. If I understand your suggestion correctly, we are streaming the User value from AuthProvider at the top, then consume the User value just below it then passing the user.uid value to the DatabaseService, which is used down the widget tree again. Looks like I am almost there from your help. If you don't mind, please let me know what you think of the provider error on the screen. Much appreciated.
You could try a switchmap from rxdart which listens to and maps a stream T to one of S.
Stream<S> switchMap<S>(Stream<S> Function(T value) mapper) =>
transform(SwitchMapStreamTransformer<T, S>(mapper));
import 'package:rxdart/transformers.dart';
final userStream = AuthProvider().user;
final itemsStream = userStream.switchMap<DocumentSnapshot>(
(user) {
if (user == null) return Stream<DocumentSnapshot>.value(null);
return itemsCollection.where('uid', isEqualTo: user.uid).snapshots();
},
);
StreamProvider<DocumentSnapshot>.value(
value: itemsStream,
),
Writing this off the cuff so it might be wrong.
Edit
Another way would just be to place your items provider one level below your user. That way you can just regularly consume the user uid value.
return StreamProvider<User>.value(
value: AuthProvider().user,
child: Consumer<User>(
builder: (_, user, __) {
if (!user) {
// Not logged in
} else {
return MultiProvider(
providers: [
Provider.value<DatabaseService>(
value: DatabaseService(user.uid),
),
// More services that rely on user.uid
],
child: SizedBox(),
);
}
},
),
);
2 Edit
class DatabaseService {
DatabaseService(this.uid);
final String uid;
getData() {
return MyCollection.where('uid', isEqualTo: uid).snapshots();
}
}
This seems to have worked having main.dart looking as below:
Main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: AuthProvider().user,
child: Consumer<User>(
builder: (context, user, __) {
if (user == null) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
title: 'Sendr',
home: LoginScreen(),
);
} else {
return MultiProvider(
providers: [
StreamProvider<DocumentSnapshot>.value(
value: DatabaseService(user.uid).userRoutesDone,
),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
title: 'Sendr',
home: NavigatorScreen(),
routes: {
SplashPage.id: (context) => SplashPage(),
LoginScreen.id: (context) => LoginScreen(),
NavigatorScreen.id: (context) => NavigatorScreen(),
MapScreen.id: (context) => MapScreen(),
},
),
);
}
},
),
);
My Notifier:
import 'package:cloud_firestore/cloud_firestore.dart';
class DatabaseService {
DatabaseService(this.uid);
final String uid;
Stream<DocumentSnapshot> get itemsInUserDocument {
final DocumentReference userData =
Firestore.instance.collection('userdata').document(uid);
return userData.snapshots();
}
}
Where I used it down the widget tree:
#override
Widget build(BuildContext context) {
final userTaskDone = Provider.of<DocumentSnapshot>(context);
List taskList = []; <-- (3) Using the list here.
taskList = userTaskDone['tasks'].toList();
print(taskList);
Related
I'm new in Flutter development, i'm making an app with Firebase Auth, in where I'm using an Authentication Wrapper class that, if user is logged in, goes to Home Screen, else goes to SignIn Screen.
The problem is that, when I want to navigate to AuthWrapper, I get this error message in a red screen:
Error: Could not find the correct Provider<UserFirebaseModel> above this Builder 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 Builder is under your MultiProvider/Provider<UserFirebaseModel>.
This usually happens when you are creating a provider and trying to read it immediately.
Here there are the most important classes of my code, where I think the problem is.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider.value(
value: Authentication().user,
initialData: UserFirebaseModel.initialData(),
child: MaterialApp(
title: 'AccessCity',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
fontFamily: 'Montserrat',
),
routes: getAppRoutes(),
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) => HomeTempPage(),
);
},
),
);
}
}
authWrapper.dart
class AuthWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<UserFirebaseModel>(context);
print(user);
// ignore: unnecessary_null_comparison
if (user == null) {
return LoginPage();
} else {
return Home();
}
}
}
routes.dart
Map<String, WidgetBuilder> getAppRoutes() {
return <String, WidgetBuilder>{
// Home Temporal Page
'/': (BuildContext context) => HomeTempPage(),
// Components Pages
'generalBigButton': (BuildContext context) => GeneralBigButtonPage(),
'textEntryField': (BuildContext context) => TextEntryFieldPage(),
'secureTextEntryField': (BuildContext context) =>
SecureTextEntryFieldPage(),
'underlinedButton': (BuildContext context) => UnderlinedButtonPage(),
// Modules
'login': (BuildContext context) => LoginPage(),
'authWrapper': (BuildContext context) => AuthWrapper(),
};
}
homeTempPage.dart
const String _title = 'Home Temporal';
const String _goAccessCityButton = 'Ir a AccessCity';
const String _goAccessCityLabel = 'Ir a pantalla Login de la app';
class HomeTempPage extends StatefulWidget {
#override
_HomeTempPageState createState() => _HomeTempPageState();
}
class _HomeTempPageState extends State<HomeTempPage> {
final model = HomeTempModel();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
backgroundColor: mainBlue,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(height: 10),
MaterialButton(
child: Text(_goAccessCityButton),
color: Colors.blue,
textColor: Colors.white,
onPressed: () {
model.navigateToStart(context);
},
),
Text(_goAccessCityLabel),
SizedBox(height: 30),
Expanded(
child: Container(
child: ListView(
children: model.getComponents(context),
),
),
),
],
),
),
);
}
}
In this last class, the line
navigateToStart() method
goes to route 'authWrapper'.
UserFirebaseModel
class UserFirebaseModel {
final String id;
final String email;
UserFirebaseModel(this.id, this.email);
factory UserFirebaseModel.initialData() {
return UserFirebaseModel('', '');
}
}
Authentication class
class Authentication {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
// Create user based on User (ex FirebaseUser)
UserFirebaseModel? _userFromFirebaseUser(User? user) {
final _mail;
if (user != null) {
_mail = user.email;
if (_mail != null) {
return UserFirebaseModel(user.uid, _mail);
}
} else {
return null;
}
}
// Auth change user stream
Stream<UserFirebaseModel?> get user {
return _firebaseAuth.authStateChanges().map(_userFromFirebaseUser);
}
// Sign in with email and password
Future<String?> signIn({
required String email,
required String password,
}) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
print("Signed in");
return "";
} on FirebaseAuthException catch (e) {
return e.message;
}
}
User? getUser() {
try {
return _firebaseAuth.currentUser;
} on FirebaseAuthException {
return null;
}
}
}
Thanks for your help, i need to solve it!
When my user signs up I direct them to a page to inform them that they need to verify their email before continuing:
Here is my verification screen code:
class VerifyScreen extends StatefulWidget {
#override
_VerifyScreenState createState() => _VerifyScreenState();
}
class _VerifyScreenState extends State<VerifyScreen> {
final auth = FirebaseAuth.instance;
User user;
Timer timer;
#override
void initState() {
user = auth.currentUser;
user.sendEmailVerification();
timer = Timer.periodic(
Duration(seconds: 5),
(timer) {
checkEmailVerified();
},
);
super.initState();
}
#override
void dispose() {
timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: MediaQuery.of(context).size.height * 0.8,
width: MediaQuery.of(context).size.width * 0.8,
child: Text(
"An email has been sent to ${user.email} please verify before proceeding"),
),
),
);
}
Future<void> checkEmailVerified() async {
user = auth.currentUser;
await user.reload();
if (user.emailVerified) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => OurHomePage(),
),
);
timer.cancel();
}
}
}
Problem Statement: When I press the back arrow on my chrome browser:
I get returned to my homepage with the user signed in which I don't want. I would like my user to verify their email before being able to continue. Here's the drawer on my homepage after I press the back button without verifying the email:
I use provider to pass my user object around my app. Here is my main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
Provider(
create: (_) => FirebaseAuthService(),
),
StreamProvider<OurUser>(
create: (context) =>
context.read<FirebaseAuthService>().onAuthStateChanged),
],
child: MaterialApp(theme: OurTheme().buildTheme(), home: OurHomePage()),
),
);
}
I then user Consumer to consume that provider on my Homepage:
class OurHomePage extends StatefulWidget {
#override
_OurHomePageState createState() => _OurHomePageState();
}
class _OurHomePageState extends State<OurHomePage> {
#override
Widget build(BuildContext context) {
return Consumer<OurUser>(
builder: (_, user, __) {
return ChangeNotifierProvider<SignInViewModel>(
create: (_) => SignInViewModel(context.read),
builder: (_, child) {
return Scaffold(appBar: AppBar(title: Text("My Homepage")));
},
);
},
);
}
}
Can anyone help me resolve the issue I'm facing? Thanks in advance.
On homepage check if user is logged in and when is, check if he has verified email. If he has, let him in, otherwise show him some message.
I am trying to take the uid from Firebase and put it in a variable, to use it in another class, but the maximum I made until now is display the value in the homepage as a text. When I try to create final String riderId = snapshot.data.uid; in the homepage, it prints the error:
The getter 'uid' was called on null.
Receiver: null
Tried calling: uid
I already saw other questions in stackoverflow and tutorials on youtube, but none of them worked for me!
My homepage:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
// print(riderId);
return StreamBuilder(
stream: AuthenticationService().authStateChanges,
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
// final String riderId = snapshot.data.uid;
if (snapshot.hasData) {
return Center(
child: Text(snapshot.data.uid),
);
} else {
return Text('Loading...');
},},);}}
My signIn button
RaisedButton(
onPressed: () {
context.read<AuthenticationService>().signIn(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
},
child: Text("Sign in"),
),
My main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AuthenticationService>(
create: (_) => AuthenticationService(),
),
StreamProvider(
create: (context) =>
context.read<AuthenticationService>().authStateChanges,
)
],
child: MaterialApp(...
My authentication service page
class AuthenticationService {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
Stream<User> get authStateChanges => _firebaseAuth.idTokenChanges();
Future<UserModel> signIn({String email, String password}) async {
var result = await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
return UserModel(result.user.uid);
}
}
Try to use authStateChanges()
Stream<User> get authStateChanges => FirebaseAuth.instance.authStateChanges();
or
Stream<User> get authStateChanges => FirebaseAuth.instance.userChanges();
The stream can emit null as a first event. So you should always access the uid after you do a null check or check if snapshot.hasData:
#override
Widget build(BuildContext context) {
// print(riderId);
return StreamBuilder(
stream: AuthenticationService().authStateChanges,
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
// final String riderId = snapshot.data.uid; // <~~ you'll always get an error in the first event if it's null
if (snapshot.hasData) {
final String riderId = snapshot.data.uid; // <~~ here should be fine
return Center(
child: Text(snapshot.data.uid),
);
} else {
return Text('Loading...');
},},);}}
By now I successfully react on auth-changes of my Firebase user to map it to my own custom user class
import 'package:mypckg/models/user.dart' as local;
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:mypckg/services/database.dart';
class AuthService {
final auth.FirebaseAuth _auth = auth.FirebaseAuth.instance;
// create user obj based on firebase user
Future<local.User> _userFromFirebaseUser(auth.User user) async {
return user != null
? local.User(/* ... init user properties ... */)
: null;
}
// auth change user stream
Stream<local.User> get user {
return _auth.authStateChanges().asyncMap(_userFromFirebaseUser);
}
...
In Cloud Firestore I store additional values of that user which are not covered by Firebase user e.g.
In main.dart the provider is set to provide my app with the local user in case he signed in or signed out (authStateChanges). My idea was to subscribe to another stream which will listen to changes on the 'users' document in Cloud Firestore.
class MyPckg extends StatelessWidget {
#override
Widget build(BuildContext context) {
final i18n = I18n.delegate;
//AuthService().signInAnon();
return MultiProvider(
providers: [
StreamProvider<User>(
create: (_) => AuthService().user,
),
/* my idea to subscribe to another stream which will listen to changes on user details in Firestore */
StreamProvider<User>(
create: (_) => AuthService().customUser,
),
],
child: DynamicTheme(
defaultBrightness: Brightness.light,
data: (brightness) {
...
I have a profile view where the user may edit those values, e.g. the locale and it gets written to the Firestore correctly with
Future<void> updateUserLanguage(String language) async {
return await usersCollection.doc(uid).update({
'language': language,
});
}
But the view is not rebuild as the current stream only reacts to authStateChanges.
Does anyone have a working example how to setup the link from users in Firestore collection that my app will listen to changes done there? What will my customUser method have to look like?
Thank you!
I've decided to wrap everything in my logged-in state with a StreamProvider:
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if (user == null) {
return Authenticate();
} else {
return StreamProvider<Profile>.value(
value: DatabaseService(uid: user.uid).profile,
initialData: Profile(userName: '0', userRole: '0'),
child: HomeScreen(),
);
}
}
}
The db service takes in the UID and returns a snapshot:
class DatabaseService {
final String uid;
DatabaseService({
this.uid,
});
static CollectionReference _profileCollection =
FirebaseFirestore.instance.collection('profiles');
Stream<Profile> get profile {
return _profileCollection.doc(uid).snapshots().map(_profileFromSnapshot);
}
Profile _profileFromSnapshot(DocumentSnapshot snapshot) {
return Profile(
userName: snapshot.data()['userName'] ?? '',
userRole: snapshot.data()['role'] ?? '',
);
}
}
Finally, I use the data via a Consumer wherever I need it:
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: Consumer<Profile>(builder: (
context,
profile,
child,
) {
return Text(profile.userName)
...
edit: I've made a video about this technique. Link: https://youtu.be/mAH9YT_y6ec
For those who are struggling with this approach as well, here is my solution (thanks to all contributors who lead me to that approach):
I set up a method "setAppUser" in MyClass which can be called from any child. Important for this
static _MyClassState? of(BuildContext context) =>
context.findAncestorStateOfType<_MyClassState>();
So here is main.dart (only the necessary sections):
class MyClass extends StatefulWidget {
#override
_MyClassState createState() => _MyClassState();
static _MyClassState? of(BuildContext context) =>
context.findAncestorStateOfType<_MyClassState>();
}
class _MyClassState extends State<MyClass> {
User appUser = User();
set setAppUser(User user) => {
setState(() {
appUser = user;
})
};
#override
Widget build(BuildContext context) {
...
return MultiProvider(
providers: [
StreamProvider<UserLogin>(
initialData: UserLogin(signedIn: false),
create: (_) => AuthService().userAuthStateChanges,
catchError: (_, error) => UserLogin(signedIn: false),
),
StreamProvider<User>(
initialData: appUser,
create: (_) => AuthService().userData,
catchError: (_, error) => appUser,
),
],
child: MaterialApp(
...
In my sign-in form I call the "setAppUser" method of my main.dart after receiving a new user of my sign-in. Now the widget gets rebuilt and the new appUser is set to my provider.
...
await _auth
.signInWithEmailAndPassword(
textControllerEmail.text,
textControllerPassword.text)
.then((result) {
if (result.id != "") {
MyClass.of(context)!.setAppUser = result;
...
I have two streams whose data I need to use app-wise.
My main obstacle is that one of the streams needs the other's data, thus, I cannot call a MultiProvider.
My current implementation looks as follows, however I do not like it: I think it is not ok to return multiple MaterialApps. Actually, my app turns black for a while, when changing from one MaterialApp to the other.
This is my current implementation:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value( //First, listen to the User Stream here
value: AuthService().user,
child: MyMaterialApp(),
);
}
}
class MyMaterialApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context); //To get the user data here, and use it bellow
if (user == null){ //If I don't have the User yet, return Loading()
return MaterialApp(
debugShowCheckedModeBanner: false,
title: "myApp",
theme: myTheme(),
home: Loading(),
);
} else {
return StreamProvider<UserData>.value(
value: DatabaseService(uid: user.uid).userData, //Once I have it, use it to build the UserData Stream
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: "myApp",
theme: myTheme(),
home: Wrapper(),
initialRoute: '/',
routes: {
'/home': (context) => Wrapper(),
//...
}
),
);
}
}
}
Thank you very much!
Based on this post https://github.com/rrousselGit/provider/issues/222 I was able to solve it by doing the following:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<User>.value(value: AuthService().user),
Consumer<User>(
builder: (context, user, child) => StreamProvider<UserData>.value(
value: DatabaseService(uid: user == null ? null : user.uid).userData,
child: child,
),
)
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: "myApp",
theme: myTheme(),
home: Wrapper(),
initialRoute: '/',
routes: {
'/home': (context) => Wrapper(),
//...
}
),
);
}
}
The Consumer listens to the User data and passes it to the next Stream.