Flutter: Problem with Firebase and Navigation - firebase

I use the next simple code for the whole process of login/register:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<FirebaseUser>.value(
value: _auth.onAuthStateChanged,
child: MaterialApp(
home: Wrapper(),
),
);
}
}
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final firebaseuser = Provider.of<FirebaseUser>(context);
// return either Home or Authenticate widget:
if (firebaseuser == null) {
return WelcomeScreen();
} else {
return StreamProvider<MyUser>.value(
value: FirestoreService(uid: firebaseuser.uid).user,
child: MaterialApp(
home: HomeWrapper(),
),
);
}
}
}
As you can see I use a second StreamProvider for my User model (with user data) which is populated at the same time it is created or logged in:
#override
Future<MyUser> signUp(String email, String password, Extrainfo info) async {
try {
AuthResult result = await _auth.createUserWithEmailAndPassword(email: email, password: password);
FirebaseUser firebaseUser = result.user;
// create a new document for the user with the uid
FirestoreService().createUser(firebaseUser.uid,email,extrainfo);
return _userFromFirebaseUser(firebaseUser);
} catch (e) {
//print(e.toString());
//return e.toString();
}
}
Normally users register/log in the WelcomeScreen() and then the second stream (of MyUser) triggers the building of HomeWrapper() (from where I have all user's data available). Users get redirected here automatically.
The problem is: everything works fine unless I introduce navigation within the WelcomeScreen(). I need to have some screens within it, but once I do that, when registering the screen does not automatically change to HomeWrapper() (Although the value of the stream gets called). This does not happen when signing in since onAuthStateChanged gets called and the re-build is triggered higher up in the widget tree.
I guess the problem is that the second Provider is outside the scope of the navigation but I do not know how to fix this in a propper way.

in a proper way, i just suggest you to navigate to the home screen only when the user has been effectively registered; i mean in the SignUpScreen.
when you will call your signUp function: with then(/*navigation callback*/) instead of using a stream in your wrapper. just remove it, and try this way.

Related

How can I use several providers for re-rendering a widget?

I'm building a Flutter app with Firebase and Riverpod.
Until the main page is displayed a user has to perform several steps to get there (e.g. sign in, validate email, get activated by admin, upload documents). For each of these steps i show a specific page or widget which is determined in AppRouterWidget (see below).
The problem i have is that i need at least 2 different providers to cover all possible states, since some aspects belong to the Firebase user in Authentication area and the others to the user account in Firebase's database (collection 'account'), which is of course only available if the user has logged in.
I can cover the authentication part, but i have no clue how i can add the user account part, which should be accessible by watching accountStreamProvider.
This is what i currently have working:
final accountStreamProvider = StreamProvider((ref) {
final database = ref.watch(databaseProvider)!;
return database.accountStream();
});
class AppRouterWidget extends ConsumerWidget {
const AppRouterWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final authStateChanges = ref.watch(authStateChangesProvider);
return authStateChanges.when (
data: (user) => _data(context, user, ref),
loading: () => const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
error: (_, __) => const Scaffold(
body: Center(
child: Text('Error'),
),
),
);
}
Widget _data(BuildContext context, User? user, WidgetRef ref) {
// user is the auth user
if (user == null) {
// either login or signup
return const AuthPage();
} else {
// logged in. now check which step we have to show
if (!user.emailVerified) {
return const VerifyEmailPage();
} else {
// these are the account specific data
//final accountAsyncValue = ref.watch(accountStreamProvider);
//if (accountAsyncValue.hasValue || !accountAsyncValue.value!.isActiv) {
// return const WeCallYouWidget();
//}
}
return Container();
}
}
}
I guess that i need 2 listeners in build() and both would call _data when triggered, but i don't know exactly how to do this.
Thanks a lot for some insights.
I'd be tempted to move all the logic for deciding which page to show outside your widget.
One way to do this would be to create a StateNotifier<PageState> subclass (PageState could be a Freezed class or an enumeration) that takes all the repositories/data sources you need as arguments, subscribes to all the streams as needed, and computes the output state that the widget can watch as:
final pageState = ref.watch(pageStateProvider);
return pageState.when(
auth: () => ....
verifyEmail: () => ...
uploadDocuments: () => ...
// and so on
);
As a result i took bizz84 advice and moved all logic into a separate class which now holds all needed listeners.
Whenever a new event happens it can react to that notification and determine the new page state which will be used to show the correct page.

InitState in flutter, deriving document value from firestore

I am working on an app where I need to filter out every user that signs in. Once they signed in, they will be redirected to wrapper that checks if the user ID exists on Firestore document collection. Here is my code.
class adminCheck extends StatefulWidget {
const adminCheck({Key? key}) : super(key: key);
#override
State<adminCheck> createState() => _adminCheckState();
}
class _adminCheckState extends State<adminCheck> {
User? user= FirebaseAuth.instance.currentUser;
bool isAdmin=false;
void initState() {
checkIfAdmin();
super.initState();
}
#override
Widget build(BuildContext context) {
if (isAdmin==true){
countDocuments();
return HomeScreen();
}else{
return notAdmin();
}
}
void checkIfAdmin() async{
DocumentSnapshot currentUser= await FirebaseFirestore.instance.collection('Administrator')
.doc(user!.email)
.get();
if(currentUser.exists){
print("This user is admin");
setState((){
isAdmin=true;
});
}
if(!currentUser.exists){
print("This user is not an admin");
}
}
}
The problem is it returns the notAdmin() class even the void method returns true which supposed to return HomeScreen(), and after few seconds, it will return HomeScreen(). I think there is a delay happening from initializing the data coming from the Firestore. Please help me with my problem. Or is there a way so that it will wait to be initialized first before returning any of those two classes?
The purpose of initState is to determine the state of the widget when it first renders. This means that you can't use asynchronous information (such as data that you still need to load from Firestore) in initState.
If the widget should only be rendered once the data is available, you should not render it until that data has been loaded. You can do this by adding another value to the state to indicate while the document is loading:
class _adminCheckState extends State<adminCheck> {
User? user= FirebaseAuth.instance.currentUser;
bool isAdmin=false;
bool isLoading=true; // 👈
And then use that in rendering:
#override
Widget build(BuildContext context) {
if (isLoading) { // 👈
return CircularProgressIndicator(); // 👈
} else if (isAdmin==true){
countDocuments();
return HomeScreen();
}else{
return notAdmin();
}
}
You can render whatever you want while the data is loading of course (or nothing at all), but the CircularProgressIndicator is typically a reasonable default.
And finally of course, you'll want to clear the loading indicator when the isAdmin is set:
void checkIfAdmin() async{
DocumentSnapshot currentUser= await FirebaseFirestore.instance.collection('Administrator')
.doc(user!.email)
.get();
if(currentUser.exists){
setState((){
isAdmin=true;
isLoading=false; // 👈
});
}
if(!currentUser.exists){
print("This user is not an admin");
}
}
This pattern is so common that it is also encapsulated in a pre-built widget. If that sounds appealing, you'll want to look at using a FutureBuilder.

StreamProvider listening to User doesn't update when User changes

In my app, I listen to changes from a User Document in Cloud Firestore.
I do this by getting the current user ID, and then getting the document associated with that ID.
class UserService {
...
//GET A USER'S INFORMATION AS A STREAM
// ? IF NO UID IS PASSED, IT GETS THE INFO OF THE CURRENT USER
Stream<User> getUserInfoAsStream({String uid}) async* {
if (uid == null) {
uid = await AuthService().getUID();
}
yield* Firestore.instance
.collection('users')
.document(uid)
.snapshots()
.map((doc) => User.fromFirestore(doc));
}
...
I then use a StreamProvider to listen to the stream in my main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<User>.value(
value: UserService().getUserInfoAsStream(),
),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: SplashScreen(),
),
);
}
}
During the course of the app's lifecycle, it works perfectly, but when the user signs out using FirebaseAuth.instance.signOut(); and then logs in with a different user, the stream remains constant (i.e it listens to the old uid stream), and the StreamProvider doesn't listen to the new stream of data.
| Sign Out Code For Reference |
// ? SIGN OUT CODE: If user signed out, it returns true, else, false
Future<bool> signOut() async {
try {
await _firebaseAuth.signOut();
return true;
} catch (error) {
print(error);
return false;
}
}
| Where it is used |
FlatButton(
onPressed: () {
AuthService().signOut().then((value) =>
Navigator.of(context).pushAndRemoveUntil(
CupertinoPageRoute(
builder: (BuildContext context) {
return Onboarding();
}), (route) => false));
},
child: Text("Yes")),
To solve the problem, I would've passed the current uid to the StreamProvider instead, but I can only get the current uid asynchronously.
How can I listen to an asynchronous stream using the StreamProvider, and update it when the user changes?
EDIT: I managed to fix the problem to some extent by moving the provider up the widget tree to the screen immediately after the login page. But because providers are scoped, I had to create a completely new MaterialApp after my original MaterialApp which is messing up my some components in my app.
Is there any better workaround?
I managed to fix the problem by switching from the provider package to get_it.
get_it allows you to register and unregister singletons, meaning that when a user logs in, I can register the singleton so it can be used across all screens that depend on it. Then, when I logout, I simply unregister it. That way, the User is always updated after signing in and out.
Here's how to do it yourself.
Install the package get_it in your pubspec.yaml.
get_it: ^4.0.2
Create a new file next to your main.dart called locator.dart. Inside it, add this code:
GetIt locator = GetIt.instance;
void setupLocator() {
// Replace this with the object you're trying to listen to.
User user;
Stream<User> userStream = UserService().getUserInfoAsStream();
userStream.listen((event) => user = event);
locator.registerLazySingleton(() => user); // Register your object
}
When you login, just call setupLocator(); and when you log out, use this code:
locator.unregister<User>();
That's all I did to get it up and running!
Edit: I managed to make it even better and lighter by using a UserProvider Singleton that listens to changes in Authentication and then gets the current user when a user logs in.
import 'package:planster/models/core/user.dart';
import 'package:planster/models/services/auth_service.dart';
import 'package:planster/models/services/user_service.dart';
class UserProvider {
// SINGLETON INITIALIZATION
static final UserProvider _singleton = UserProvider._internal();
factory UserProvider.instance() {
return _singleton;
}
UserProvider._internal() {
listenToUserAuthState();
}
// VARIABLES
User user;
void listenToUserAuthState() async {
AuthService().onAuthStateChanged.listen((event) {
uid = event.uid;
if (uid != null)
UserService().getUserInfoAsStream(uid: uid).listen((userEvent) {
user = userEvent;
});
});
}
}

What is the cleanest way to access a logged in Firebase user across your Flutter app?

I am using firebase in my flutter app and accessing the logged in user would be done something like this -
Future<FirebaseUser> getLoggedInUser() async {
return await FirebaseAuth.instance.currentUser();
}
I use BLOC and was thinking of adding this to my AuthenticationBloc class. Since this BLOC is injected early, I should be able to access this from anywhere in the app -
AuthenticationBloc authBloc = BlocProvider.of<AuthenticationBloc>(context);
FirebaseUser user = await authBloc.getLoggedInUser()
But this leads to a few problems like accessing this in a StreamBuilder becomes an issue because of the need of using await which means I would need to make the StreamBuilder async (not sure how).
What would be the best/cleanest way of accessing the user object FirebaseUser user from anywhere in the app ?
Thanks !
Edit 1 :
So one way to do this would be to use a stateful widget every time I want the current user
class _MyWidgetState extends State<MyWidget> {
FirebaseUser user;
#override
void initState() {
super.initState();
_updateCurrentUser();
}
void _updateCurrentUser() async {
FirebaseUser currUser = await Firebase.instance.currentUser();
setState(() {
user = currUser;
});
}
But using a stateful widget every time I want the currUser seems weird. Is there a better way out ?
You can access the user by using stream such as
StreamBuilder(
stream:FirebaseAuth.instance.onAuthStateChanged,
builder:(context,snapshot){
if (snapshot.connectionState == ConnectionState.active) {
final user = snapshot.data;
return Text("User Id:${user.uid}");
}else{
return Center(
child:CircularProgressIndicator(),
);
}
}
);

Returning null user data from Firestore. How to reference it globaly instead?

I'm quite new to Flutter and I've been struggling to access a user's document on Firestore.
On the profile page,
I'm setting the current user's UID inside initState, but uid returns null for a quick second, then the page updates with correct info.
So I am able to retrieve a certain field (like displayName), but it isn't quite the best practice. I don't want to have a bunch of boilerplate code and await functions mixed with UI and such.
Code:
FirebaseUser user;
String error;
void setUser(FirebaseUser user) {
setState(() {
this.user = user;
this.error = null;
});
}
void setError(e) {
setState(() {
this.user = null;
this.error = e.toString();
});
}
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then(setUser).catchError(setError);
}
Then in my body I have a Stream builder to get the document.
body: StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(user.uid)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.deepOrange),
),
);
} else {
var userDocument = snapshot.data;
return showProfileHeader(userDocument);
}
},
)
I want to make 'global' references to be accessed throughout the app. Instead of getting the user's id on every page and streaming a specific field when I might need multiple ones.
The only ways I found online to do something similar, created lists with all the data in it. I feel like this might get extra fields I don't need.
How can I make data from Firestore available across the app?
I am using the "Provider" package for doing state management across my app. Nowadays its also the suggested way by the google flutter team when it comes to state management. See the package here: https://pub.dev/packages/provider
Regarding Firebase Auth and accessing the credentials application wide, i am using that said package like stated on this page:
https://fireship.io/lessons/advanced-flutter-firebase/
Short version below. Bootstrap your app like so:
import 'package:provider/provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Make user stream available
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged),
// not needed for your problem but here you can see how
// to define other Providers (types) for your app.
// You need a counter class which holds your model of course.
ChangeNotifierProvider(builder: (_) => Counter(0)),
],
// All data will be available in this child and descendents
child: MaterialApp(...)
);
}
}
Then in your child widgets, just do:
// Some widget deeply nested in the widget tree...
class SomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<FirebaseUser>(context);
return Text(user.displayName) // or user.uid or user.email....
}
}
This should do the trick.
That happens because FirebaseAuth.instance.currentUser() returns a future, and until that future is completed, you will not have the proper FirebaseUser object.
Making the user object global is not a bad idea. In addition, you can hook it up to the FirebaseAuth stream so that it gets updated everytime the user auth status changes, like so in a user.dart file:
class User {
static FirebaseUser _user;
static get user => _user;
static void init() async {
_user = await FirebaseAuth.instance.currentUser();
FirebaseAuth.instance.onAuthStateChanged.listen((firebaseUser) {
_user = firebaseUser;
});
}
}
You can call User.init() in main() and access the user object with User.user.

Resources