Fetch data from firestore after Sign In before showing HomeScreen - flutter

When a User sign up for the first time, i want that he gets a own firestore document with some data. This data I want to show on the homescreen but I get an error that the data is not there yet. After hot reload the data is there so the problem is that the homescreen is shown before the data is fetched from firestore although I use a FutureBuilder. I only got this problem when a user signs in for the first time.
class GoogleSignInProvider extends ChangeNotifier {
final googleSignIn = GoogleSignIn();
GoogleSignInAccount _user;
GoogleSignInAccount get user => _user;
Future googleLogin() async {
try {
final googleUser = await googleSignIn.signIn();
if (googleUser == null) return;
_user = googleUser;
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
await FirebaseAuth.instance.signInWithCredential(credential);
await getUserData();
} catch (e) {
print(e.toString());
}
notifyListeners();
}
Future getUserData() async {
final myUser = FirebaseAuth.instance.currentUser;
bool userExist = await checkIfUserExists(myUser.uid);
if (userExist == false) {
print('User dont exist');
await FirebaseFirestore.instance.collection('users').doc(myUser.uid).set({
"email": myUser.email,
"plans": [],
"userScore": "100",
});
} else {
print('User exist');
}
/// Save userData from firestore in a Helper class which is shown on the homescreen
var userData = FirebaseFirestore.instance.collection('users').doc(myUser.uid);
return FutureBuilder(
future: userData.get(),
builder: (context, userDataSnapshot) {
if (userDataSnapshot.data == ConnectionState.done) {
var value = userDataSnapshot.data;
UserManager.userdata = value.data(); //static class where userData is stored
return null;
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
});
}
Future<bool> checkIfUserExists(String uid) async {
try {
var collectionRef = FirebaseFirestore.instance.collection('users');
var doc = await collectionRef.doc(uid).get();
return doc.exists;
} catch (e) {
throw e;
}
}
EDIT
This is the Button where the user can sign In
ElevatedButton.icon(
onPressed: () {
final provider = Provider.of<GoogleSignInProvider>(context, listen: false);
provider.googleLogin();
},
icon: Icon(MdiIcons.google),
label: Text(
'Sign In with Google',
style: TextStyle(fontSize: 16),
),
),

The problem here is that FutureBuilder is a widget, it should not be used in a function to wait for a future to complete, but in another widget to have callbacks on the completion and change display based on that.
If not rendered, FutureBuilder will do nothing but be instantiated and occupy memory.
You should probably modify your code as such:
...
/// Save userData from firestore in a Helper class which is shown on the homescreen
var userData = await FirebaseFirestore.instance.collection('users').doc(myUser.uid).get();
UserManager.userdata = userData.data();
...
Should you want to add a CircularProgress on your main screen, this would be done by lisntening to your Provider in some way.

Related

How to use sharedpreferences to save users roles and navigate to a specific page depending on role in Flutter

I'm working on app that have user logins (Admin login and user login). First i make a user part and it works, the account keep logged even when the app restart. and then when i have to separate the users (admin and user) i got some problem. I don't know how to code the shared preferences, this is the code when i make a user part
preference_helper.dart
import 'package:shared_preferences/shared_preferences.dart';
class PreferencesHelper {
final Future<SharedPreferences> sharedPreferences;
const PreferencesHelper({required this.sharedPreferences});
static const String login = 'LOGIN';
void setIsLogin(bool value) async {
final prefs = await sharedPreferences;
prefs.setBool(login, value);
}
Future<bool> get isLogin async {
final prefs = await sharedPreferences;
return prefs.getBool(login) ?? false;
}
}
i use the provider like this
preference_notifier.dart
class PreferencesNotifier extends ChangeNotifier {
PreferencesHelper preferencesHelper;
PreferencesNotifier({required this.preferencesHelper}) {
_getIsLogin();
}
bool _isLogin = false;
bool get isLogin => _isLogin;
void _getIsLogin() async {
_isLogin = await preferencesHelper.isLogin;
notifyListeners();
debugPrint(_isLogin ? 'isLogin true' : 'isLogin false');
}
void setIsLogin(bool value) async {
preferencesHelper.setIsLogin(value);
_getIsLogin();
}
}
i want to use shared preferences to save the user roles and navigate to specific page. So if the user's log in it will go to the UserHomePage and if the admin log in it will go to the AdminHomePage. My backend is firebase firestore.
this is part of sign page (when click register button)
MaterialButton(
color: primaryColor,
textTheme: ButtonTextTheme.primary,
height: 40,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
onPressed: () async {
setState(() {
_isLoading = true;
});
try {
final navigator = Navigator.of(context);
final email = _emailController.text;
final password = _passwordController.text;
const role = "user";
await _auth
.createUserWithEmailAndPassword(
email: email,
password: password,
)
.then((value) => {postDetailsToFirestore(email, role)});
navigator.pop();
} catch (err) {
final snackBar = SnackBar(content: Text(err.toString()));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} finally {
setState(() {
_isLoading = false;
});
}
},
child: const Text('Signup'),
),
postDetailsToFirestore(String email, String role) async {
FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;
var user = _auth.currentUser;
CollectionReference ref = firebaseFirestore.collection('users');
ref.doc(user!.uid).set({'email': _emailController.text, 'role': role});
}
this is the login page (when click the login button)
MaterialButton(
color: primaryColor,
textTheme: ButtonTextTheme.primary,
height: 40,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
onPressed: () async {
setState(() {
_isLoading = true;
});
try {
final navigator = Navigator.of(context);
final email = _emailController.text;
final password = _passwordController.text;
await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
route();
value.setIsLogin(true);
navigator.pushReplacementNamed(HomePage.routeName);
} catch (err) {
final snackBar = SnackBar(content: Text(err.toString()));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} finally {
setState(() {
_isLoading = false;
});
}
},
child: const Text('Login'),
),
i want to navigate the navigator to specific user role
this is the route() function
void route() {
User? user = FirebaseAuth.instance.currentUser;
FirebaseFirestore.instance.collection('users').doc(user!.uid).get().then(
(DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists) {
if (documentSnapshot.get('role') == "user") {
Navigator.pushNamed(context, UserHomePage.routeName);
} else {
Navigator.pushNamed(context, AdminHomePage.routeName);
}
} else {
debugPrint('Document does not exist on the database');
}
},
);
}
and this is the main.dart at runApp()
runApp(
await preferencesHelper.isLogin
? const MyApp(
pageRouteName: HomePage.routeName,
)
: const MyApp(
pageRouteName: LoginPage.routeName,
),
);
I really need to know how am i supposed to do because this is for my exam. I'm sorry if my english is bad, i'm barely use English to talk. Thank you
that code that i share is what i tried to make sharedpreferences but it just for 1 user, i dont know how to separate user (admin and user)
First of all, you need to use architecture to separate the UI from logic and in your architect, you have to create a layer to handle basic requests of the local database and then create a class for implementing basic commands of the database, then you can create a separated storage layer for each of entities that you have.
the abstract basic commands class is like this :
abstract class LocalStorage {
Future<void> write(final String key, final dynamic json);
dynamic read<S>(final String key);
void remove(final String key);
void removeAll();
}
and for implementation :
class StorageService implements LocalStorage {
StorageService() {
_init();
}
late GetStorage storage;
void _init() {
storage = GetStorage();
}
#override
Future<void> write(final String key, final dynamic value) async {
await storage.write(key, convert.jsonEncode(value));
}
#override
dynamic read<S>(final String key) {
final value = storage.read(key);
if (value == null) return;
return convert.jsonDecode(value.toString());
}
#override
void remove(final String key) {
GetStorage().remove(key);
}
#override
void removeAll() {
GetStorage.Remove(key1);
GetStorage.Remove(key2);
...
}
}
and for Usage for each entity:
class UserStorage {
final LocalStorage _storage;
Future<void> SaveUser(User usr) async {
await _storage.write(userKey, usr);
}
}
I have used GetX to handle local storage for read and write but you can replace your preferred shared preference library.

Flutter Getx: google signin and map data to firebase automatically logs me back in as same user?

I am trying to login with google and have the data mapped to a firebase user. I'm using getX. So far this works HOWEVER it automatically logs me back in as the same user if I logout and then try to log back in again. I'll send the code for my login page and the page where the logout button is if needed, but I suspect this may have to do with my AuthController which I've included here
class AuthController extends GetxController {
static AuthController instance = Get.find();
GoogleSignIn googleSignIn = GoogleSignIn();
Rxn<User> firebaseUser = Rxn<User>();
Rxn<UserModel> firestoreUser = Rxn<UserModel>();
final RxBool admin = false.obs;
String usersCollection = "users";
#override
void onReady() async {
//run every time auth state changes
ever(firebaseUser, handleAuthChanged);
firebaseUser.bindStream(user);
super.onReady();
}
handleAuthChanged(firebaseUser) async {
//get user data from firestore
if (firebaseUser?.uid != null) {
firestoreUser.bindStream(streamFirestoreUser());
print("You are logged in as ${firebaseUser.email}");
await isAdmin();
}
//this is for new users
if (firebaseUser == null) {
print('Send to signin');
Get.offAll(LoginPage());
} else {
Get.offAll(AppSetup());
}
}
// Firebase user one-time fetch
Future<User> get getUser async => auth.currentUser!;
// Firebase user a realtime stream
Stream<User?> get user => auth.authStateChanges();
//Streams the firestore user from the firestore collection
Stream<UserModel> streamFirestoreUser() {
print('streamFirestoreUser()');
return firebaseFirestore
.doc('/users/${firebaseUser.value!.uid}')
.snapshots()
.map((snapshot) => UserModel.fromSnapshot(snapshot));
}
//get the firestore user from the firestore collection
Future<UserModel> getFirestoreUser() {
return firebaseFirestore
.doc('/users/${firebaseUser.value!.uid}')
.get()
.then((documentSnapshot) => UserModel.fromSnapshot(documentSnapshot));
}
//Method to handle user sign in using email and password
// User registration using email and password
googleLogin(BuildContext context) async {
final GoogleSignInAccount? googleUser = await googleSignIn.signIn();
if (googleUser != null) {
final googleAuth = await googleUser.authentication;
if (googleAuth.accessToken != null && googleAuth.idToken != null) {
try {
await auth
.signInWithCredential(
GoogleAuthProvider.credential(
idToken: googleAuth.idToken,
accessToken: googleAuth.accessToken),
)
.then((firebaseUser) async {
print('uid: ' + firebaseUser.user!.uid.toString());
print('email: ' + firebaseUser.user!.email.toString());
//create the new user object from the login modelled data
UserModel _newUser = UserModel(
id: firebaseUser.user!.uid,
email: firebaseUser.user!.email!,
name: firebaseUser.user!.email!,
photoURL: firebaseUser.user!.photoURL,
cart: [],
);
//create the user in firestore here with the _addUserToFirestore function
_updateUserFirestore(_newUser, firebaseUser.user!);
});
} on FirebaseAuthException catch (error) {
Get.snackbar('auth.signUpErrorTitle'.tr, error.message!,
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 10),
backgroundColor: Get.theme.snackBarTheme.backgroundColor,
colorText: Get.theme.snackBarTheme.actionTextColor);
}
}
}
}
void _updateUserFirestore(UserModel user, User _firebaseUser) {
firebaseFirestore.doc('/users/${_firebaseUser.uid}').update(user.toJson());
update();
}
updateUserData(Map<String, dynamic> data) {
logger.i("UPDATED");
firebaseFirestore
.collection(usersCollection)
.doc(firebaseUser.value!.uid)
.update(data);
}
//check if user is an admin user
isAdmin() async {
await getUser.then((user) async {
DocumentSnapshot adminRef =
await firebaseFirestore.collection('admin').doc(user.uid).get();
if (adminRef.exists) {
admin.value = true;
} else {
admin.value = false;
}
update();
});
}
// This is the proper sign out method!
Future<void> signOut() {
return auth.signOut();
}
}
Simply add this line of code into your logout function
> await googleSignIn.signOut()

Flutter How to check whether the user sign up with google provider before?

I want to check whether the user has signed up before or not
(if this is his first time I will send him to write some data if not he will enter to the main page of the app).
GoogleSignInAccount _user;
GoogleSignInAccount get user => _user;
Future googlelogin() async {
try {
final googleUser = await googleSignIn.signIn();
if (googleUser == null) return;
_user = googleUser;
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
await FirebaseAuth.instance.signInWithCredential(credential);
} catch (e) {
print(e.toString());
}
notifyListeners();
}
I also have a StreamBuilder to check the data that user enter
StreamBuilder<Object>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator.adaptive(),
);
} else if (snapshot.hasData) {
return LoggedInWidget();
} else if (snapshot.hasError) {
return Center(
child: Text('Something went wrong'),
);
} else {
return SignUp();
}
})
You can use the pre-built method "isNewUser" of the "UserCredential" class. Call the sing in with credentials function from a variable and use said variable to perform the check.
...
var result =
await FirebaseAuth.instance.signInWithCredential(credential);
if (result.additionalUserInfo!.isNewUser) {
// Perform what you need to do for new users here
// like creating a user document
}else {
//Perform what you want to do for old users here
//like fetching a specific user document
}
...

State managment in flutter with consumer and scaffoldState

I'm using the Provider dependencie to manage states on my screen. Currently I have created a Loading Screen that works with Lottie animation. In my Sign In page, whenever there is an error with the log in, a Snackbar is shown to the user. Althought now, when I use the splash screen, the screen doesn't return and the snackBar isn't shown.
This is a piece of the login screen:
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: RaisedButton(
onPressed: userManager.loading
? null
: () {
if (formKey.currentState!
.validate()) {
userManager.signIn(
user: User(
email:
emailController.text,
password:
passController.text),
onFail: (e) {
scaffoldKey.currentState!
.showSnackBar(SnackBar(
content: Text(
'Falha ao entrar: $e'),
backgroundColor:
Colors.red,
));
},
onSucess: () {
debugPrint(
'Sucesso ao Logar');
Navigator.of(context).pop();
});
}
},
On the onFail I get this error, whenever I have a wrong password or other datas wrong:
Ocorreu uma exceção.
_CastError (Null check operator used on a null value)
This is how I'm changing between pages:
class LoginScreen extends StatelessWidget {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController emailController = TextEditingController();
final TextEditingController passController = TextEditingController();
#override
Widget build(BuildContext context) {
return Consumer<UserManager>(builder: (_, userManager, child) {
if (userManager.loading) {
return SplashScreen();
} else {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
UserManager:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:loja_virtual_nnananene/helpers/firebase_errors.dart';
import 'package:loja_virtual_nnananene/models/user.dart';
class UserManager extends ChangeNotifier {
UserManager() {
_loadCurrentUser();
}
final FirebaseAuth auth = FirebaseAuth.instance;
User? user;
bool _loading = false;
bool get loading => _loading;
bool get isLoggedIn => user != null;
Future<void> signIn(
{required User user,
required Function onFail,
required Function onSucess}) async {
loading = true;
try {
final AuthResult result = await auth.signInWithEmailAndPassword(
email: user.email!, password: user.password!);
await _loadCurrentUser(firebaseUser: result.user);
onSucess();
} on PlatformException catch (e) {
onFail(getErrorString(e.code));
}
loading = false;
notifyListeners();
}
Future<void> signUp(
{required User user,
required Function onFail,
required Function onSucess}) async {
loading = true;
try {
final AuthResult result = await auth.createUserWithEmailAndPassword(
email: user.email!, password: user.password!);
user.id = result.user.uid;
this.user = user;
await user.saveData();
onSucess();
} on PlatformException catch (e) {
onFail(getErrorString(e.code));
}
loading = false;
notifyListeners();
}
void signOut() {
auth.signOut();
user = null;
notifyListeners();
}
set loading(bool value) {
_loading = value;
notifyListeners();
}
Future<void> _loadCurrentUser({FirebaseUser? firebaseUser}) async {
final FirebaseUser currentUser = firebaseUser ?? await auth.currentUser();
if (currentUser != null) {
final DocumentSnapshot docUser = await Firestore.instance
.collection('users')
.document(currentUser.uid)
.get();
user = User.fromDocument(docUser);
final docAdmin = await Firestore.instance
.collection('admins')
.document(user!.id!)
.get();
if (docAdmin.exists) {
user!.admin = true;
}
notifyListeners();
}
}
bool get adminEnabled => user != null && user!.admin;
}
Is there another way to set the splash screen thats easier?
While I wait for you to add the UserManager class implementation, I think there's a missing notifyListeners() in the signIn method.

Creating StreamBuilder to keep track whenever there is a change on Sensor value with Realtime Firebase

I'm working on a project to keep track of temperature and humidity sensor whenever they change. I will be working with a ESP32 to send the data to the Firebase, and my App to keep monitoring the values. So if the value goes from 23 to 24 I would like to immediately show the user on my app that change.
I will use a StreamBuilder to keep track of theses changes, But I'm having problems using the Stream.
This is how I the code I'm using to gather the specific user sensor info. This code is at a separate dart file, called auth.dart
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final databaseReference = FirebaseDatabase.instance.reference();
//Cria um objeto baseado no Usuario da FirebaseUser
User _userFromFirebaseUser (FirebaseUser user){
return user != null ? User(uid: user.uid) : null;
}
// Devolve o UID da pessoa
Future<String> personuid() async{
final FirebaseUser user = await _auth.currentUser();
return user.uid;
}
// Função para ler o valor da temperatura
Future<int> getSensorTemperature() async {
final FirebaseUser user = await _auth.currentUser();
int result = (await databaseReference.child(user.uid+"/temperature").once()).value;
print(result);
return result;
}
// Função para ler o valor da humidade
Future<int> getSensorHumidity() async {
final FirebaseUser user = await _auth.currentUser();
int result = (await databaseReference.child(user.uid+"/humidity").once()).value;
print(result);
return result;
}
In my home page I attempted to use the StreamBuilder like this:
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final AuthService _auth = AuthService();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Main page'),
actions: <Widget>[
FlatButton.icon(onPressed: () async {
await _auth.signOut();
},
icon: Icon(Icons.logout),
label: Text('Logout'))
],
),
body: StreamBuilder(
stream: _auth.getSensorTemperature(), <-- I get an error here
builder: (context, snapshot) {
if (snapshot.hasError){
return Container(color: Colors.red,);
}
if (!snapshot.hasData){
return Center(child: CircularProgressIndicator(),);
}
if (snapshot.hasData){
return Container(
color: Colors.blue,
);
}
},
),
);
}
}
The error I get from the StreamBuilder is:
The argument type 'Futureint' can't be assigned to the parameter type 'Streamdynamic'
You're not actually using streams. You were only taking a single event previously. Use the streams that the package make available and then use an await for to handle it.
Stream<int> getSensorTemperature() async* {
final FirebaseUser user = await _auth.currentUser();
await for(var event in databaseReference.child(user.uid+"/temperature").onValue) {
yield event.snapshot.value;
}
}
With error handling:
Stream<int> getSensorTemperature() async* {
final FirebaseUser user = await _auth.currentUser();
Stream stream = databaseReference.child(user.uid+"/temperature").onValue.handleError((error) {
print("Error: $error");
});
await for(var event in stream) {
yield event.snapshot.value;
}
}