StreamBuilder taking too long to update (or not updating at all) - firebase

I use firebase authentication in my app and I have a widget called AuthGate that the app launches as its home. In AuthGate there is just a StreamBuilder that listens to FirebaseAuth.instance.authStateChanges() and returns different screens depending on the user state. When no user is logged in, the LoginScreen is returned, and when they log in through one of the methods, the StreamBuilder should send them to the HomeScreen. The same should happen when registering and when they sign out it should automatically send them back to the LoginScreen. My issue is that sometimes the changes are not detected at all, and even when they are, it takes at least a few seconds. Is there a way to make it update faster? I like this method a lot more than the way I used to do it (pushing replacements), because it's cleaner and more efficient, so I hope there is a solution. Here is the code in the AuthGate:
class AuthGate extends StatelessWidget {
const AuthGate({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const LoginScreen();
} else if (snapshot.data?.displayName == null ||
snapshot.data?.email == null ||
snapshot.data?.photoURL == null) {
return UpdateProfileScreen(user: snapshot.data!);
} else {
return HomeScreen(user: snapshot.data!);
}
});
}
}

Related

How to check if a user document exists in Flutter Firestore?

I'm trying to add a check after a user is authenticated if a user has a document in Firestore under a 'users' collection based on their UserID ('uid') via a StreamBuilder.
The issue I have is when I run my code, it works as intended but after a few seconds, it redirects to the 'UserHomeScreen' even if the document does not exist. How can I rectify this so a user without a user document does not get pushed to my 'UserHomeScreen'?
Here is my code:
class UserStream extends StatelessWidget {
const UserStream({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('users').doc('uid').snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return const UserHomeScreen();
} else {
return const SignUpNewUser();
}
},
);
}
}
The snapshot.hasData is true when the asynchronous call has completed. Even when the document doesn't exist, snapshot.hasData will still be true once that has been determined.
To ensure the document exists, you'll also want to check:
if (snapshot.hasData && snapshot.data!.exists) {
...
This is also shown in the documentation on handling one time reads.

How to wait for homepage in flutter until firestore check query?

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

Flutter firebase auth doesn't persist after app is killed

I am trying to persist the firebase auth state in a flutter app by using this code from the documentation but when I kill the app in the emulator and open it again it doesn't recognize a user.
I can use sharedpreferences but I want to use only firebase, what am I doing wrong here?
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// Create the initialization Future outside of `build`:
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
final FirebaseAuth auth = FirebaseAuth.instance;
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return FutureBuilder(
// Initialize FlutterFire:
future: _initialization,
builder: (context, snapshot) {
// Check for errors
if (snapshot.hasError) {
return (MaterialApp(
home: Warning(
warning: 'Error',
),
));
}
// once complete show your app
if (snapshot.connectionState == ConnectionState.done) {
print('CONNECTED');
if (AuthService().user() == null) {
return MaterialApp(
home: LoginPage(),
);
} else {
return MaterialApp(
home: HomePage(),
);
}
}
// if nothing happens return loading
return MaterialApp(
home: //LoginPage()
Warning(
warning: 'Loading',
),
);
},
);
}
}
AuthService class
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
// auth change user stream
User user() {
// ignore: deprecated_member_use
_auth.authStateChanges().listen((User user) {
if (user == null) {
return null;
} else {
return user;
}
});
}
}
I hope you can help me to understand the problem and solve it, thank you.
Since authStateChanges returns a Stream, you'll want to use a StreamBuilder in your code to wrap that asynchronous operation too.
Something like this:
// once complete show your app
if (snapshot.connectionState == ConnectionState.done) {
print('CONNECTED');
return StreamBuilder(
stream: FirebaseAuth.instance. authStateChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
return MaterialApp(
home: LoginPage(),
);
} else {
return MaterialApp(
home: HomePage(),
);
}
}
)
}
Unrelated: you're repeated the code to create a MaterialApp quite frequently, which is not needed. For example, in the above snippet we could have only one mention of MaterialApp and get the same result with:
// once complete show your app
if (snapshot.connectionState == ConnectionState.done) {
print('CONNECTED');
return StreamBuilder(
stream: FirebaseAuth.instance. authStateChanges(),
builder: (BuildContext context, snapshot) {
return MaterialApp(
home: snapshot.hasData && snapshot.data != null ? HomePage() : LoginPage(),
)
}
)
}
If you do this for all mentions of MaterialApp and other duplication, you can reduce the code significantly, making it less error prone and easier to maintain.
It does persist. You are just not using the auth state listener correctly. The return statements from your authStateChanges listener are not actually escaping the call to user(). On top of that, the listener could return null the first time. It's not until some time later that the Firebase SDK determines that the user is actually valid and signed in. Your listener will get a second callback at that time. Your code need to be ready for this to happen - it can't just blindly take the first value, as the auth state might change over time.
I suggest adding some debug logging in your auth state listener to see how this actually works. Also I suggest reading this blog to understand how auth state listeners work in more detail.
You can use my code, You can use userChanges() instead of authStateChanges()
final Stream<User?> firebaseUserChanges = firebaseAuth.userChanges();

Flutter : Why does the login page appear briefly before going to home screen every time I restart my app although the user is already logged in?

Flutter & Firebase : Why does the login page always appear briefly before going to home screen every time I restart my app even though the user is already logged in?
I understand that my app needs sometimes to render the user from firebase. But the user is already logged anyway. So how can I proceed the home screen immediately since the user is already logged in? Is there a way to save the user data into the phone memory?
class _WrapperState extends State<Wrapper> {
#override
Widget build(BuildContext context) {
final user = Provider.of<SystemUser>(context);
if (user == null) {
print('In Authenicate or Login');
return Authenticate();
} else {
print('In HomeScreen');
return NavigationWrapper(); // goto homescreen
}
}
}
Please have a look and give some pointers.
You can save user auth state in shared preferences, but even in that case you will need to load some data before navigate to auth screen or home screen.
I recommend you use the following solution. This way you load auth state and then navigate the corresponding pages according to the auth state.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class _WrapperState extends State<Wrapper> {
#override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: Provider.of<SystemUser>(context).isAuthenticate(),
builder: (context, snapshot) {
// while loading data
if (snapshot.data == null) {
return CircularProgressIndicator();
}
// if has error
if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
// retrieve data - check for authentication
// authenticated, go to homescreen
if (snapshot.data == true) {
print('In HomeScreen');
return NavigationWrapper();
}
// not authenticated, go to auth screen
print('In Authenicate or Login');
return Authenticate();
},
);
}
}
// in SystemUser provider
// check for auth state and return corresponding value
Future<bool> isAuthenticate() async {
// you can implement shared prefereces
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('authKey') ?? false;
}
// whenever you logged in the user just call
prefs.setString('authKey', true);
What I suggest You to do is to use StreamBuilder to check if the user is logged in or not like this:
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, AsyncSnapshot<User> snapshot) {
if(snapshot.connectionState == ConnectionState.waiting)
return Center(child: CupertinoActivityIndicator());
else if(!snapshot.hasData || snapshot.data == null)
return LoginPage();
else if (snapshot.hasError)
return Center(child: Text('${snapshot.error}'));
return HomePage();
},
);
}
}
I use it like this in every project that needs FirebaseAuth, I hope it helps.
You can do the following using the Consumer element.
Install the Provider package for flutter and make a bool isAuth. When the user logs in, set isAuth = true, then add an if statement inside home: like in the image I provided. Hopefully that will solve your problem.

why it doesn't keep the user logged in

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

Resources