Flutter Firebase Auth - change home widget if the user is logged in - firebase

I'm building an app that supports only google login using google_sign_in package. When the app is run, it first checks if FirebaseAuth is alive. If user in FirebaseAuth.instance.authStateChanges().listen((User? user) is not null, HomePage() should be shown. If the user is null it should go to AuthPage() which has a google sign-in button.
The code for main.dart is shown below.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
Widget _defaultHome = HomePage();
// Check if user already logged in
FirebaseAuth auth = FirebaseAuth.instance;
auth.authStateChanges().listen((User? user) {
if (user == null) {
_defaultHome = AuthPage();
}
print(user);
});
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => SignInProvider()),
ChangeNotifierProvider(create: (_) => DataProvider()),
],
child: new MaterialApp(
title: 'Mentea',
home: _defaultHome,
routes: {
'auth': (context) => AuthPage(),
'home': (context) => HomePage(),
},
),
));
}
But when I run it, console gives
I/flutter (14933): null
which means no logged in user, but the emulator shows HomePage() instead of AuthPage(). Anybody know how to fix this? I tried changing 'home:' attribute to 'initialRoute:', but it doesn't work either (directs to HomePage() while printing null).
String _defaultHome = 'home';
...
auth.authStateChanges().listen((User? user) {
if (user == null) {
_defaultHome = 'auth;
}
});
...
child: new MaterialApp(
title: 'Mentea',
initialRoute: _defaultHome,
routes: {
'auth': (context) => AuthPage(),
'home': (context) => HomePage(),
},
),

We can assign a StreamBuilder that listens to FirebaseAuth state changes, to the home property:
StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (user) {
return user == null ? AuthPage() : HomePage(),
}
),
Follows a full example:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => SignInProvider()),
ChangeNotifierProvider(create: (_) => DataProvider()),
],
child: MaterialApp(
title: 'Mentea',
home: StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (user) {
return user == null ? AuthPage() : HomePage(),
},
),
routes: {
'auth': (context) => AuthPage(),
'home': (context) => HomePage(),
},
),
));
}

Related

Flutter streamprovider issue with userdata from Firebase

I've been stuck in this issue for sometime now and after lots of tries, now i don't know where the issue is.
The problem is i'm using StreamProvider to get userData from firebase and then use that userData through provider throughout my project, Now everything works fine but the issue arises when i try to log out. Actually even logout works perfectly, untill i close the app using the backbutton of my phone and then reopen the app from open apps list, then when i try to logout it doesn't work correctly. When i refresh the app the user is logged out but initially it doesn't navigate to the login screen.
here is the logout code.
InkWell(
onTap: () async {
await showDialog(
context: context,
useRootNavigator: false,
builder: (ctx) => AlertDialog(
title: const Text("Are you Sure?"),
content: const Text("Do You want to logout?"),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
"No",
style: TextStyle(
color: ColorConstant.appColor,
),
),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop(true);
await AuthService().signOut();
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: ((context) =>
const LoginScreen())),
(route) => false);
},
child: const Text(
"Yes",
style: TextStyle(
color: ColorConstant.appColor,
),
),
),
],
),
);
},
And here is the main.dart i've defined StreamProvider.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
final _auth = AuthService();
final String? _userId = _auth.user?.uid;
return MultiProvider(
providers: [
StreamProvider<List<Packages>>(
create: (_) => DatabaseService().packages, initialData: const []),
StreamProvider<UserModel?>(
create: (_) => DatabaseService().userData(_userId),
initialData: null,
),
],
builder: (context, child) {
return MaterialApp(
title: 'DreamaX',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: ColorConstant.appColor,
errorColor: Colors.red,
),
home: const Wrapper(),
);
},
);
}
}
Also one another thing that when i'm logging in, i navigate to MyApp so that the StreamProvider can get the userId and initialize the streamprovider.
here is the login code
try {
await _authService.signIn(
emailCont: _emailCont.text.trim(),
passCont: _passCont.text.trim());
Navigator.of(context)
.pushAndRemoveUntil(
MaterialPageRoute(
builder: ((context) =>
const MyApp())),
(route) => false);
} on FirebaseAuthException catch (e) {
So i'm really confused now what could be the cause here?

Not changing screen based on Authentication Status Flutter

I am trying to go to the home screen or stay on auth screen based on whether or not the user is successfully authenticated. Using Firebase authentication's authStateChanges and a stream builder.
I get no error codes and the console reads
D/FirebaseAuth(21665): Notifying id token listeners about user ( pG6pORODSGMi21fuaoql29hqXZp2 ).
D/FirebaseAuth(21665): Notifying auth state listeners about user ( pG6pORODSGMi21fuaoql29hqXZp2 ).
so the authentication is successful and when I hot restart the app it goes to the home screen and displayed info as it should.
Here is the code
FutureBuilder(
future: _initialization,
builder: (context, appsnapShot) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: {
SignupScreen.routeName: (ctx) => SignupScreen(),
AuthScreen.routeName: (ctx) => AuthScreen(),
HomeScreen.routeName: (ctx) => HomeScreen(),
UserPhoneAdds.routeName: (ctx) => UserPhoneAdds(),
PhoneAddForm.routeName: (ctx) => PhoneAddForm(),
EditPhoneScreen.routeName: (ctx) => EditPhoneScreen(),
},
home: appsnapShot.connectionState != ConnectionState.done
? SplashScreen()
: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return SplashScreen();
}
if (snapshot.hasData) {
return HomeScreen();
} else {
return AuthScreen();
}
},
),
);
});
FYI-this is off of a course and in the app it came from this works correctly
Thank you for any help.
The authStateChanges doesn't return a snapshot but a User as you can see in the officaial documentation:
FirebaseAuth.instance
.authStateChanges()
.listen((User user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
So you sould just check if User is null or not.

UID call on null and provider failing to pass on data with TypeError (type 'FirebaseUser' is not a subtype of type 'String')

I tried to reach user data to determine Admin role and pass on to Future Builder. Depending on Admin, the result will be determine if which widget will be available.
floatingActionButtonLocation:
FloatingActionButtonLocation.centerDocked,
floatingActionButton:
FutureBuilder(
future: _getProfileData(user, authNotifier),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
_isAdmin = snapshot.data['isAdmin'] ?? false;
}
return Container(
child: Column(
children: <Widget>[
adminFeature(),
]
));
}),
The widget below provides information on user.
Widget adminFeature() {
if(_isAdmin == true) {
return
FloatingActionButton(
backgroundColor:CompanyColors.blue[500],
child: const Icon(Icons.add),onPressed: () {
var foodNotifier = Provider.of<FoodNotifier>(context, listen: false);
foodNotifier.currentFood = null;
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) {
return FoodForm(
isUpdating: false,
);
}),
);
});
} else {
return Container();
}
}
_getProfileData(User user, AuthNotifier authNotifier) async {
final uid = await Provider.of<AuthNotifier>(context, listen: false).getCurrentUser();
await Firestore.instance
.collection('Users')
.document(uid)
.get().then((result) {
user.isAdmin = result.data['isAdmin'];
});
}
Below, this is the provider. It works for all the apps, but I fail to get the user data and pass on to a widget.
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => AuthNotifier(),
),
ChangeNotifierProvider(
create: (context) => FoodNotifier(),
),
ChangeNotifierProvider(create: (context) => ThemeProvider(isLightTheme: false)),
ChangeNotifierProvider(create: (context) => UserxProvider()),
ChangeNotifierProvider(create: (context) => UsersProvider()),
],
child: MyApp());
}
Notifier is mentionned below:
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
Future<String> getCurrentUID() async {
return (await _firebaseAuth.currentUser()).uid;
}
// GET CURRENT USER
Future getCurrentUser() async {
return await _firebaseAuth.currentUser();
}
In your _getProfileData method, you should change the following:
await Provider.of(context, listen: false)
to
await Provider.of<UserxProvider>(context, listen: false)
// or UsersProvider, whichever you're trying to call
The problem is that when you call Provider without specifying the model that you want (i.e., the type), it becomes Provider.of<dynamic> and hence you're getting that error message.

Pass user uid to Firestore stream query in Flutter

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);

How to call a function ASA the app runs in flutter?

I have an application that do login with the help of firestore database and I want to do autologin so I made a boolean and set it to false in the database and made the login function set it to true as he or she sign in, so I want to check if the person have already signed in or not as the app runs, any ideas :) ?
here my code:
void getUserData() async {
try {
var firebaseUser = await FirebaseAuth.instance.currentUser();
firestoreInstance
.collection("Students")
.document(usernameController.text)
.get()
.then((value) {
setState(() {
email = (value.data)['email'];
password = (value.data)['password'];
gender = (value.data)['gender'];
loggedin = (value.data)['token'];
});
});
} catch (e) {
print(e.toString);
}
}
You dont have to use a boolean to check if the user is logged in or not. Firebase authentication already offers that. You can check inside the initState:
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then((res) {
print(res);
if (res != null) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => Home(uid: res.uid)),
);
}
else
{
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SignUp()),
);
}
});
}
Checks if there is a current user or not and navigates to the required page.
If you have different types of users, then you have to identify them in the database. So authenticate in firebase authentication, and use a userType field in the database:
void registerToFb() {
firebaseAuth
.createUserWithEmailAndPassword(
email: emailController.text, password: passwordController.text)
.then((result) {
firestoreInstance.collection("users").document(result.user.uid).setData({
"email": emailController.text,
"name": nameController.text,
"userType" : "Students"
}).then((res) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => Home(uid: result.user.uid)),
);
});
}).catchError((err) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Error"),
content: Text(err.message),
actions: [
FlatButton(
child: Text("Ok"),
onPressed: () {
Navigator.of(context).pop();
},
)
],
);
});
});
}

Resources