App Flowchart
I have a question about async function in flutter. I write an that use Firebase authentication. I want to make it such that the app will read the Firebase User ID at the top level of the app(Root Page in this case) at the init state function and then pass the user object to its child widget. Since the function to retrieve the user ID is an async function, I run into problem that the child widget get a null value for user ID even though it should not be null. I have already use future builder in the children widget but it doesn't work. Does anyone know how to do it correctly.
The exact error I am getting is "A build function returned null. The offending widget is: FutureBuilder. Build functions must never return null."
RootPage (Parent)
class _RootPageState extends State {
AuthStatus authStatus = AuthStatus.notSignIn;
String cuerrentUserId;
#override
void initState() {
super.initState();
widget.auth.currentUser().then((userId) {
setState(() {
authStatus = userId == null ? AuthStatus.notSignIn : AuthStatus.signIn;
cuerrentUserId = userId;
});
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
switch(authStatus) {
case AuthStatus.notSignIn:
return new LoginPage(
auth: new Auth(),
onSignedIn: _signedIn,
);
case AuthStatus.signIn:
if (snapshot.connectionState == ConnectionState.done) {
return new HomePage(
auth: widget.auth,
onSignedOut: _signedOut,
userId: snapshot.data.uid,
);
}
else {
}
}
}
),
);
}
HomePage (child)
Future<String> setUserData() async {
currentUser = User(widget.userId);
await currentUser.loadUserData();
_userName = currentUser.name;
_userEmail = currentUser.email;
_userPicURL = currentUser.avatar;
print('current user');
print(currentUser.id);
print(currentUser.email);
return _userName;
}
#override
Widget build(BuildContext context) {
return UserProvider(
user: currentUser,
child: new Container(
child: new FutureBuilder(
future: setUserData(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.data!=null) {
...
You could make your main function async in order to decide during app launch if you should show the login or home page as the first screen.
This could look like the following:
Future<void> main() async {
FirebaseUser currentUser = await FirebaseAuth.instance.currentUser();
bool showHomePage = currentUser != null;
runApp(MyApp(showHomePage));
}
You could use the showHomePage param inside MyApp now to determine which screen should be shown initially. That's it.
Bonus: With this approach you also don't need to show a screen for a friction of a second which may be replaced by another one (e.g. show the home screen --> user is not logged in --> replace with login screen). This could look like a glitch in your app.
Related
Here i implement firebase login, register, home page setting in my flutter app.
For this i want to set Homepage according to firestore query in main.dart
Here is my code
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
//
return MaterialApp(
debugShowCheckedModeBanner:false,
home: setHomePage(),
);
}
}
setHomePage() {
if(FirebaseAuth.instance.currentUser==null){
return Login();
}
else {
FirebaseFirestore.instance.collection(AppString.FB_USERS).doc(
FirebaseAuth.instance.currentUser.uid).get().then((
DocumentSnapshot snapshot) {
if (snapshot.exists) {
return UserStateJunction();
}
else {
return User();
}
});
}
}
I think this code has no error ,
But this is not work and homepage is not waiting for firestore query , it throws null exception
Could not find a generator for route RouteSettings("/", null) in the
_WidgetAppState. Make sure your root app widget has provided a way to generate this route. Generators for routes are searched for in the following order:
For the "/" route, the "builder" property, if non-null, is used.
Otherwise, the "routes" table is used, if it has an entry for the route.
Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "builder" and "routes".
Finally, if all else fails onUnknownRoute is called. Unfortunately, onUnknownRoute was not set.
int main(){
runApp( YourApp() )
}
class YourApp extends StatelessWidget{
#override
Widget build(BuildContext context){
return FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot){
if (snapshot.hasData){
FirebaseUser user = snapshot.data; // this is your user instance
/// is because there is user already logged
return MainScreen();
}
/// other way there is no user logged.
return LoginScreen();
}
);
}
}
I am trying to build a flutter app with Firestore .
I am trying to write a code wherein if a document if exists in Firestore in a collection then the user goes to a new screen if not he goes to an other screen
FirebaseAuth auth = FirebaseAuth.instance;
class check extends StatelessWidget {
static const String routeName = '/checkif';
#override
Widget build(BuildContext context) {
final firebaseUser = context.watch<User>();
final snapshot = fb.collection("Profile").doc(firebaseUser.uid).get();
if (snapshot == null) {
return addparentcompany();
} else{
return homepage();}
}
}
Even if the snapshot is null even then this gets routed to homepage instead of parent company
Because it takes time to fetch the data, You will have to wait for the data while its being retrieve.. So for that you'll have to use the FutureBuilder
body: FutureBuilder(
future: fb.collection("Profile").doc(firebaseUser.uid).get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return addparentcompany();
} else {
if (snapshot.data.data() == null) {
return Center(
child: Text('An error occured'),
);
} else return homepage();
}
},
),
I am using the authStateChanges stream from Firebase with flutter. I have two views, one for mobile and the another one for a web application. I want to redirect the user to the SignIn screen if he is not connected, logged in or authenticated. At first it works well but then when i am logged in and refresh the browser i got the SignIn screen loaded for like 1 second and then the Web screen appears again. I checked with print what's going on and from what i saw, the authStateChanges Stream is null for that 1-2 seconds(when SignIn screen appears) and then has a value when the stream receives the connected user. Is there a way to check, or wait until this authentication is done before loading the SignIn screen when it must not load it ?
My main component contains the StreamBuilder as following:
Widget build(BuildContext context) {
final firebaseAuthService = Provider.of<FirebaseAuthService>(context);
return StreamBuilder<User>(
stream: firebaseAuthService.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
User user = snapshot.data;
if (user == null) {
//first time no connection
return SignIn();
}
if (kIsWeb) {
return WebMain(user: user);
}
// load mobile version
return MobileMain();
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
});
}
Here you can find my FirebaseAuth wrapper class which contains the methods from firebase:
class FirebaseAuthService {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
User _user;
bool get isAuthenticated {
return _user == null ? false : true;
}
User get user {
return _user;
}
Future<User> signInWithEmailAndPassword(
String userEmail, String userPassword) async {
return _user = await _firebaseAuth
.signInWithEmailAndPassword(email: userEmail, password: userPassword)
.then((userCredential) => userCredential.user);
}
Stream<User> authStateChanges() {
_user = _firebaseAuth.currentUser;
return _firebaseAuth.authStateChanges();
}
Future<void> signOut() async {
return _firebaseAuth.signOut();
}
}
While I am not sure why authStateChanges does not notify when the user sign in state is changed (usually a second later), a similar function does seem to work for your use case.
Try idTokenChanges()
FirebaseAuth.instance.idTokenChanges().listen((event) {
print("On Data: ${event}");
});
This event will return your Firebase User object. When refreshed, it might return 'null' initially, but within a second, returns your signed in User. You could potentially make the sign in page wait a couple of seconds to make sure a signed in user isn't being initialized.
EDIT:
While there may be better solutions, this is currently working for me.
final subscription = FirebaseAuth.instance.idTokenChanges().listen(null);
subscription.onData((event) async {
if(event != null) {
print("We have a user now");
isLoading = false;
print(FirebaseAuth.instance.currentUser);
subscription.cancel();
Navigator.push(
context,
MaterialPageRoute(builder: (context) => OverviewController())
);
} else {
print("No user yet..");
await Future.delayed(Duration(seconds: 2));
if(isLoading) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => LoginController())
);
isLoading = false;
subscription.cancel();
}
}
});
For me, the below code seems to work fine. Although there is a warning in docs that says "You should not use this getter to determine the user's current state, instead use [authStateChanges], [idTokenChanges] or [userChanges] to subscribe to updates."
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Diary Book',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
primarySwatch: Colors.green,
),
home: (FirebaseAuth.instance.currentUser == null)
? LoginPage()
: MainPage(),
);
}
}
I haven't encountered any issues using the above code. I Will let you know if do. If someone can comment any future errors this may have that would be great
FirebaseAuth.instance.authStateChanges().listen(
(event) {
if (event == null) {
print('----user is currently signed out');
} else {
print('----user is signed in ');
}
runApp(
const MyApp()
);
},
);
it doesnt matter if the user is logged in or not it goes to the main page.
anf if i make the login page the home page everytime i restart the app it requires to login again.
i want it to be like once log in then be logged in till you log out
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: await getLandingPage(),
routes: {
'upload': (context) => ItemInput(),
'suzuki': (context) => Suzuki(),
'others': (context) => Others(),
},
));
}
Future<Widget> getLandingPage() async {
final FirebaseAuth _auth = FirebaseAuth.instance;
return StreamBuilder<User>(
stream: _auth.authStateChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData && (!snapshot.data.isAnonymous)) {
return MainPage();
}
return LoginPage();
},
);
}
When the app is started, Firebase automatically restores the user's authentication state. This may take a few moments, as it needs to check with the server whether the account is still active.
During this time, the user will not be signed in yet, so authStateChanges() fires a null. And that's when your code redirects the user to the login page.
You'll want to either wait for a few moments to see if the user state is restored, or move/copy your navigation logic to the login page, so that it redirects to the main page once the user authentication state is restored.
Following on from Frank's answer, this is how to work around the issue of receiving a null on the first authStateChanges() event using a StreamBuilder widget.
#override
Widget build(BuildContext context) {
return StreamBuilder<User>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return _buildWaitingScreen();
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
final firebaseUser = snapshot.data;
if (firebaseUser != null) {
//....
}
return SignInPage();
}
}
});
}
If you don't want to use a switch statement, you can check only ConnectionState.active
#override
Widget build(BuildContext context) {
return StreamBuilder<User>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
//...
}
return _buildWaitingScreen();
});
}
According to the docs:
A stream A source of asynchronous data events.
A Stream provides a way to receive a sequence of events. Each event is either a data event, also called an element of the stream, or an error event, which is a notification that something has failed. When a stream has emitted all its event, a single "done" event will notify the listener that the end has been reached.
Since the authStateChanges() returns a Stream then you can use the StreamBuilder to get the result and display the widgets accordingly.
According to the StreamBuilder docs:
As an example, when interacting with a stream producing the integers 0 through 9, the builder may be called with any ordered sub-sequence of the following snapshots that includes the last one (the one with ConnectionState.done):
new AsyncSnapshot.withData(ConnectionState.waiting, null)
new AsyncSnapshot.withData(ConnectionState.active, 0)
new AsyncSnapshot.withData(ConnectionState.active, 1)
...
new AsyncSnapshot.withData(ConnectionState.active, 9)
new AsyncSnapshot.withData(ConnectionState.done, 9)
Therefore the builder of type AsyncWidgetBuilder which is used for asynchronous operation, will call your widgets according to the state of the Stream, for example:
#override
Widget build(BuildContext context) {
final FirebaseAuth _auth = FirebaseAuth.instance;
return new Scaffold(
body: StreamBuilder(
stream: _auth.authStateChanges(),
builder: (context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData)
return MainPage();
else
return LoginPage();
} else
return Center(
child: CircularProgressIndicator(),
);
}));
}
You can use the above code in the splash screen, here the Stream will be in the waiting state where it will display a loading first, and then when it retrieves the data, if it is either null or if there is a user logged in, it will enter the active state and return a widget which satisfies the condition.
https://api.flutter.dev/flutter/widgets/AsyncWidgetBuilder.html
https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
https://api.flutter.dev/flutter/widgets/StreamBuilder/builder.html
After talking with OP. They are using the following plugin google_sign_in, and have an auth.dart file with the following code:
void signOutGoogle() async {
await googleSignIn.signOut();
}
What happened in that case, is that the user signed out from Google auth but was still logged in inside Firebase, so to solve this you can add:
void signOutGoogle() async {
await googleSignIn.signOut();
await _auth.signOut();
}
I am trying to implement a Home class which shows a Login screen for new users or auto-logins previously signed in users and brings them to directly to the app. However, when the code runs it shows login screens for signed in users.
From my understanding, the Future function is initially returning a null and the code finishes with "Login Page" being shown even though the Future later returns the current User.
class Home extends StatelessWidget {
FirebaseAuth _auth = FirebaseAuth.instance;
Future<FirebaseUser> getCurrentUser() async {
return _auth.currentUser();
}
Widget userLoggedIn() {
getCurrentUser().then((user) {
if (user != null) {
//User is auto-logged in = build main app
return new Scaffold(
body: Center(
child: Text("Main App"),
),
);
} else if (user == null) {
//New user = return null
return null;
});
}
#override
Widget build(BuildContext context) {
return userLoggedIn() ??
//New user = build login page
new Scaffold(
body: Center(
child: Text("Login Page"),
),
);
}
}
Could you please help me fix this?
You can start the auto-login progress in your LoginPage's initState,
LoginPage must be StatefulWidget to use initState
#override
void initState() {
FirebaseAuth.instance.currentUser().then((user) {
if (user != null) { //if there isn't any user currentUser function returns a null so we should check this case.
Navigator.pushAndRemoveUntil(
// we are making YourHomePage widget the root if there is a user.
context,
MaterialPageRoute(builder: (context) => YourHomePage()),
(Route<dynamic> route) => false);
}
});
super.initState();
}
signOut with that method:
FirebaseAuth.instance.signOut()
You need to wait until _auth.currentUser() returns the user.
Use await keyword and not .then().
Change your getCurrentUser() method.
Return a FirebaseUser and not a Future.
FirebaseUser currentUser = await authService.getCurrentUser();