Nested StreamBuilders Flutter - flutter

So I'm currently using this nest of two streams, one to listen for AuthStateChanges, to know if the user is logged in, and another that listens to a firebase document snapshot request, to know if the user has already setup is account or not.
My problem is that the latter StreamBuilder(_userStream) only runs if the firts one runs, meaning that the only way for my _userStream to run is if the user either logs in or logs out(authStateChanges Stream).
This is inconvinient because after the user creates an account(moment where i run Auth().createUserWithPasswordAndEmail()), I need the user to go throw the process of seting up the account, and only after that the user can acess the mainPage. Only in the end of seting up the account theres a button to "Create Account", which changes the "HasSetupAccount" parameter in firebase to true. But because of the nested Streams problem, the app doesn't go to the mainPage until I force update it.
I hope my question is not as confusing as it looks :)
class _WidgetTreeState extends State<WidgetTree> {
#override
//construtor da class?
Widget build(BuildContext context) {
return StreamBuilder(
stream: Auth().authStateChanges,
builder: (context, snapshot) {
if (snapshot.hasData) {
return StreamBuilder(
stream: _userStream(),
builder:
((context, AsyncSnapshot<DocumentSnapshot> userSnapshot) {
if (userSnapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else {
Map<String, dynamic> userData =
userSnapshot.data!.data() as Map<String, dynamic>;
print(userSnapshot.data!.data().toString());
if (userData['HasSetupAccount'] == true) {
return MyHomePage();
} else {
return AccountSetup();
}
}
}));
} else {
return LoginPage();
}
},
);
}
Stream<DocumentSnapshot<Map<String, dynamic>>> _userStream() {
return FirebaseFirestore.instance
.collection('Users')
.doc(Auth().currentUser!.uid)
.snapshots();
}
}

Related

Provider cannot handle Firebase authStateChanges()

I am struggeling for the long time with handling correctly reauthentication of user in conjunction with storing the data in Provider.
During the first execution of the app on the device, the user is unauthenticated. Then user can register/login and re-build of the class below occure. Unfortunately, even throu the re-build occur, also when the document in Firestore changes, the change does not reflect in the Provider object or is reflected, but only when user does full reload of the app (depending on the scenario).
Here is my code:
class LandingFlowWidget extends StatefulWidget {
const LandingFlowWidget({Key? key}) : super(key: key);
#override
State<LandingFlowWidget> createState() => _LandingFlowWidgetState();
}
class _LandingFlowWidgetState extends State<LandingFlowWidget> {
late UserData? _userData;
#override
void initState() {
super.initState();
_userData = UserData();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return ProgressIndicatorWidget(color: Color(0xFF3030D0));
} else if (snapshot.hasError) {
return ErrorScreen();
} else if (snapshot.hasData &&
(FirebaseAuth.instance.currentUser != null &&
FirebaseAuth.instance.currentUser!.isAnonymous == false))
return VerifyEmailScreen();
else {
if (FirebaseAuth.instance.currentUser == null)
return OnboardingScreen();
return ChangeNotifierProvider<UserData?>(
create: (context) => _userData,
builder: (context, _) {
return StreamBuilder<UserData>(
stream: FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser?.uid)
.snapshots()
.map((snap) => UserData.fromJson(snap.data()!)),
builder: (BuildContext context,
AsyncSnapshot<UserData> snapshot) {
if (snapshot.hasError) {
return ErrorScreen();
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
return ProgressIndicatorWidget(
color: Color(0xFF3030D0));
} else {
_userData = snapshot.data;
_userData?.updateState();
return OnboardingScreen();
}
});
});
}
});
}
}
I experimented with different approaches:
Changing Provider to ChangeNotifierProvider
StreamProvider insted of Provider + StreamBuilder in the function below
StreamProvider in the MultiProvider in main.dart with empty Stream or correct stream and adding new stream to StreamController when re-authentication occure.
I tried to look on the internet and did not find working solution of Provider + Change of Authentication. I'd appreciate some code snippets.
I found a very ugly workaround.
In main.dart, I created MultiProvider that contains StreamProvider:
MultiProvider(
providers: [
(...)
StreamProvider<UserData>.value(
value: FirebaseAuth.instance.currentUser == null
? Stream.empty()
: FirebaseClient.userStream,
initialData: UserData(),
),
(...)
],
(...)
The stream:
static Stream<UserData> userStream = FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser?.uid)
.snapshots()
.map((snap) => UserData.fromJson(snap.data()!));
As mentioned in my initial question, this code does not work when re-authentication occurs but starts working again when the user does the full reload of the app. Having said that, when re-authentication occurs, I leverage Phoenix package to reload the entire app. Then the stream builds again with the correct user uid, and everything works as expected.
I would still appreciate it if someone could suggest a more elegant solution.

Check if collection has been created and navigate using FutureBuilder Firebase Firestore Flutter

class TestData extends StatelessWidget {
const TestData({super.key});
#override
Widget build(BuildContext context) {
final uid = FirebaseFirestore.instance.collection('users').doc();
return FutureBuilder(
future: FirebaseFirestore.collection("user").doc(uid).get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return AddProfileScreen();
} else {
if (snapshot.hasError) {
return Center(
child: Text('An error occured'),
);
}
if (snapshot.hasData && !snapshot.data!.exists) {
return AddProfileScreen();
}
if (snapshot.hasData) {
return MainPage();
} else
return CircularProgressIndicator();
}
},
);
}
}
im trying to make a one time form after registration that create the collection and then after user fill all the forms they directed to mainscreen().
i don't know how to make it, any suggestions?
i already make the form and can store the data into collection but i want to validate if user already has collection data they immediately directed to main screen so the form become one time only.

Flutter Firestore boolean value check

Well, I want to check if the profile is complete after creating the account so I added a bool to the firestore. When the user fills in all the data and clicks "complete" at the end, then bool "complete" will be true and I did it, but now I want to check before the user starts filling in the data if bool is true or false. If this is true, the user will be redirected to the dashboard, if it is false, he will have to complete all the data after logging in. User login details are stored in firebase and the rest of the information is stored in firestore.
If any more information is needed, I will try to specify it
I would like to check if the value is true or false before redirecting to "CreateProfile1 ();", if it's possible
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: ((context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return Center(child: Text('Something went wrong!'));
} else if (snapshot.hasData) {
return CreateProfile1();
} else {
return AuthPage();
}
}),
));
}
}
I was trying to save bool value into variable, but i've got this error
external static Never _throw(Object error, StackTrace stackTrace);
Here is this var, final actually
final complete = FirebaseFirestore.instance
.collection('usersdData')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get()
.then((value) {
if ((value.data() as dynamic)['complete'] == true) {
return true;
} else {
return false;
}
});

Stream builds a stack of Widget

So, I am using a stream to track the user's authentication state. Here is my setup, which works fine so far.
class Root extends ConsumerWidget {
final Widget _loadingView = Container(color: Colors.white, alignment: Alignment.center, child: UiHelper.circularProgress);
#override
Widget build(BuildContext context, ScopedReader watch) {
return watch(userStreamProvider).when(
loading: () => _loadingView,
error: (error, stackTrace) => _loadingView,
data: (user) => user?.emailVerified == true ? Products() : Login(),
);
}
}
The problem is, stream builds the UI multiple times. And I have a welcome dialog inside of my products page, which opens multiple times and as soon as I start the app it becomes a mess.
What should I do to avoid this scenario?
** Here I am using riverpod package
I personally recommend wrapping your widget with a StreamBuilder using the onAuthStateChanged stream. This stream automatically updates when the user change its state (logged in or out). Here is an example that may help you!
Stream<FirebaseUser> authStateChanges() {
FirebaseAuth _firebaseInstance = FirebaseAuth.instance;
return _firebaseInstance.onAuthStateChanged;
}
return StreamBuilder(
stream: authStateChanges(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// isLoggedIn
} else if (snapshot.hasData == false &&
snapshot.connectionState == ConnectionState.active) {
// isLoggedOut
} else {
// loadingView
}
},
);

Firestore Class 'QuerySnapshot' has no instance method '[]'

I want a ListView to show the names of the users. I am using a cloudfunction with the admin sdk to return a list of all the users with the corresponding user IDs. When I want to pass that uid to a Widget with a streambuilder, it gives me the error:
Class 'QuerySnapshot' has no instance method '[]'.
Receiver: Instance of 'QuerySnapshot'
Tried calling: []("firstName")
This is the function I am calling while building the ListView for the title:
Widget getFirstName(uid, item) {
return StreamBuilder(
stream: Firestore.instance
.collection('users')
.document('HzBUMs06BAPHK0Y7m5kfOmUzawC2')
.collection('userInfo')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return Text('${item['email']}');
} else {
return Text('${snapshot.data.documents['firstName']}');
}
},
);
}
I am not using the uid which I will pass to it yet, as the User ID that I hardcoded right now is the only one with the firstName data in it.
When I feed it a non-existing userID, it still seems to think it has data in it and tries to return its (non-existent) data.
What am I doing wrong here?
I managed to fix it by using this piece of code:
Widget fullNameWidget(uid) {
return FutureBuilder(
future: fullName(uid),
builder: (context, snapshot) {
return Text('${snapshot.data}');
},
);
}
Future fullName(uid) async {
return Firestore.instance
.collection("users")
.document('$uid')
.collection("userInfo")
.getDocuments()
.then((querySnapshot) {
print(querySnapshot.documents[0]['firstName']);
if (querySnapshot == 'null') {
} else {
return '${querySnapshot.documents[0]['firstName']} ${querySnapshot.documents[0]['lastName']}';
}
// querySnapshot.documents.where((element) {
// print(element.documentID == 'firstName');
// });
});
}