StreamBuilder snapshot is inactive on creating own stream - flutter

It is a firebase authentication system with email verification. I am using 2 streams, one is firebase authstatechanges and one of my own to check mail verification status.
I am using a stream combiner for combining 2 streams.
On combining 2 streams and calling the function in stream builder, the snapshot was returning an inactive state. It was not even entering the method "combinestream". Then I tried checking my own stream and passed it to streambuilder alone this time. I found out that it was giving the same problem.
Here is my stream controller code with 2 streams combined:
class AuthService {
final userVerificationStreamController = StreamController<bool>.broadcast();
final auth.FirebaseAuth _firebaseAuth = auth.FirebaseAuth.instance;
MyUser? _userFromFirebase(auth.User? newUser) {
if (newUser == null) {
return null;
}
print("^^^^^------?>>>>>>>>> ${newUser.uid} ,, ${newUser.email} ,, ${newUser.emailVerified}");
return MyUser(newUser.uid, newUser.email,newUser.emailVerified);
}
Stream<CombineStreamer> get combineStream {
Stream<MyUser?> firebaseAuthStream = _firebaseAuth.authStateChanges().map(_userFromFirebase);
Stream<bool> userVerificationStream = userVerificationStreamController.stream;
print("######___>>>>>>>>>> COMBINING STREAMS");
return firebaseAuthStream.combineLatest(userVerificationStream, (p0, p1){
if(p0==null){
userVerificationStreamController.sink.add(false);
}
else if(p0.isEmailVerified){
userVerificationStreamController.sink.add(true);
}
if (p1.toString().toLowerCase() == 'true') {
print("______+++++++___****** ${p1.toString().toLowerCase()}");
return CombineStreamer(p0,true);
} else{
print("______+++++++___>>>>>> ${p1.toString().toLowerCase()}");
return CombineStreamer(p0,false);
}
}
.....
....
}
I called this combineStream method in wrapper class like:
Widget build(BuildContext context) {
final authService = Provider.of<AuthService>(context, listen: false);
return StreamBuilder(
stream: authService.combineStream,
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.connectionState == ConnectionState.active){
print("!!!!___--->>>>> THE STATE IS ACTIVE");
CombineStreamer? streamedData = snapshot.data;
if(streamedData!=null) {
if(streamedData.user == null || !streamedData.isUserVerified){
print("!!!!___--->>>>> GOING TO LOGIN PAGE");
return LoginPage();
}
else{
print("!!!!___--->>>>> GOING TO HOMEPAGE");
return HomePage();
}
}
else{
print("!!!!!!###### SNAPSHOT IS NULL");
return const Scaffold(body: CircularProgressIndicator(color: Colors.red,),);
}
}
else{
print("!!!!___--->>>>> SHOWING CIRCULAR INDICATOR");
return const Scaffold(body: Center(child: CircularProgressIndicator(),));
}
},
);
}
Here it ALWAYS shows me the circular progress indicator of blue color the default one, where the snapshot is inactive. I am not able to figure out what's causing this problem. Please look into this.
And add the comment if you think something is missing. Thanks!

Related

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

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

Retrigger FutureBuilder on Login / Registration

I'm currently stuck at what I hope is a simple problem, I just tried so much that I probably just can't see the solution any longer.
I have a Landing Page that checks via a future whether the user has an active session or not (Parse Backend).
I manage to make successful login and registration requests, just the screen doesn't change, meaning the future builder doesn't rebuild. When I hot reload everything works fine, but I don't manage to automatically trigger the hot reload. I user Riverpod for state management.
The hasUserLogged() Method is supplied via Riverpod by an AuthBase class.
I hand over the updatedUser method to the AuthScreen to trigger it on login/signUp, but it doesn't trigger a rebuild of the FutureBuilder.
I thought getting an updatedUser from Server would also supply me in the next step with information whether the user has its email verified, but that's the follow up problem (but I would appreciate a pointer in the right direction how to solve the 4x4 user matrix: has token / no token & verified / unverified e-mail and redirecting to Auth / Verify E-Mail / HomePage depending on combinations..)
Anyhow, for now - how can I trigger the rebuild of the FutureBuilder upon Login/SignUp Button press in the AuthScreen?
class LandingPage2 extends StatefulWidget {
#override
_LandingPage2State createState() => _LandingPage2State();
}
class _LandingPage2State extends State<LandingPage2> {
Future<ParseUser> _updateUser() async {
final auth = context.read(authProvider);
ParseUser currentUser = await ParseUser.currentUser() as ParseUser;
if (currentUser != null) {
ParseResponse update = await currentUser.getUpdatedUser();
if (update.success) {
currentUser = update.result as ParseUser;
await auth.hasUserLogged();
setState(() {
return currentUser;
});
}
}
if (currentUser == null) {
print('null User');
}
}
/// Check if user session token is valid
Future<bool> hasUserLogged() async {
ParseUser currentUser = await ParseUser.currentUser() as ParseUser;
// return false if no user is logged in
if (currentUser == null) {
return false;
}
//Validates that the user's session token is valid
final ParseResponse parseResponse =
await ParseUser.getCurrentUserFromServer(
currentUser.get<String>('sessionToken'));
if (!parseResponse.success) {
print('invalid session. logout');
//Invalid session. Logout
await currentUser.logout();
return false;
} else {
print('login successfull');
return true;
}
}
#override
Widget build(BuildContext context) {
final auth = context.read(authProvider);
return FutureBuilder<bool>(
future: auth.hasUserLogged(),
builder: (context, snapshot) {
print('futurebuilder rebuild');
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return SplashScreen();
break;
default:
if (snapshot.hasData && snapshot.data) {
return HomePage();
} else {
return AuthScreen(_updateUser);
}
}
},
);
}
}
Any help is highly appreciated, struggle since hours and my head can't wrap around why it is not working :-/
Thank you #Randal Schwartz, 'watch' made it happen, after I created an AuthNotifier and StateNotifierProvider to manage the user state and depending on that user in the hasUserLogged() method.
If anyone is also struggling - that's the working version:
import 'dart:async';
import 'package:app/screens/splash_screen.dart';
import 'package:app/services/top_level_providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:parse_server_sdk_flutter/parse_server_sdk.dart';
import 'auth_screen.dart';
import 'home.dart';
class LandingPage2 extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final auth = watch(authProvider);
/// Check if user session token is valid
Future<bool> hasUserLogged() async {
print('hasUserLogged - Fired');
/// watch current User state -- Triggers rebuild after login!
final authNotifier = watch(authNotifierProvider.state);
final ParseUser currentUser = authNotifier;
// return false if no user is logged in
if (currentUser == null) {
print('currentUserNULL');
return false;
}
//Validates that the user's session token is valid
final ParseResponse parseResponse =
await ParseUser.getCurrentUserFromServer(
currentUser.get<String>('sessionToken'));
if (!parseResponse.success) {
print('invalid session. logout');
//Invalid session. Logout
await currentUser.logout();
return false;
} else {
print('login successfull');
return true;
}
}
return FutureBuilder<bool>(
future: hasUserLogged(),
builder: (context, snapshot) {
print('futurebuilder rebuild');
// print(snapshot.data);
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return SplashScreen();
break;
default:
if (snapshot.hasData && snapshot.data) {
/// Add Verify E-Mail Logic here - Another Future Builder??
return HomePage();
} else {
// _updateUser();
return AuthScreen();
}
break;
}
},
);
}
}
The Auth Notifier:
/// Auth Notifier Class
class AuthNotifier extends StateNotifier<ParseUser> {
AuthNotifier(ParseUser state) : super(state);
setCurrentUser(ParseUser user) {
state = user;
}
void clearUser() {
state = null;
}
}
And the provider:
final authNotifierProvider = StateNotifierProvider((ref) {
return AuthNotifier(null);
});
This is triggered after the active User after login / registration is received and thus triggers rebuild of hasUserLogged.
authNotifier.setCurrentUser(user);
Appreciate the help! Did cost me a lot of time... Having to switch away from firebase sucks...

Dart/Flutter: How to return widget from async function?

I have a simple app from which a user can login and on logging in, a token is generated and stored on the device.
When the app starts, the following code below runs.
import 'package:coolkicks/screens/authpage.dart';
import 'package:coolkicks/screens/homescreen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:logger/logger.dart';
class Authenticate extends StatefulWidget {
#override
_AuthenticateState createState() => _AuthenticateState();
}
class _AuthenticateState extends State<Authenticate> {
final storage = new FlutterSecureStorage();
var log = Logger();
bool authenticated = false;
void checkToken() async {
String token = await storage.read(key: 'token');
if (token == null || token.length == 0) {
authenticated = false;
} else {
authenticated = true;
print(token);
log.d(token);
log.i(token);
}
}
#override
Widget build(BuildContext context) {
//check if Authenticated or Not
//return either Products Home Screen or Authentication Page
//If token exists, return Home screen
//Else return authpage
checkToken();
if(authenticated) {
return HomeScreen();
}
else {
return AuthPage();
}
}
}
My issue is that retrieving the token returns a future and takes some time to execute.
So it always returns the default authenticated = false
You should use FutureBuilder
FutureBuilder<String>(
future: storage.read(key: 'token'),
builder: (context, snapshot) {
if (snapshot.hasData) {
final token = snapshot.data;
if (token == null || token.length == 0) {
return HomeScreen();
} else {
return AuthPage();
}
}
if (snapshot.hasError) return WidgetThatShowsError();
// by default show progress because operation is async and we need to wait for result
return CircularProgressIndicator();
},
);
Don't do this. build should be idempotent. You should call checkToken() in initState. Then you can either use setState or you use a FutureBuilder.
But, provided the naming, you should rather just provide a splash screen, check the condition and navigate to either screen, instead of using 1 route for both screens.

Flutter: Future<String> returns null but if printing it has value

username is null but if I'm printing 'value' it contains some string, how can I get 'value'?
class HomeWrapper extends StatelessWidget {
final DataBaseServices _db = DataBaseServices();
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
String username;
_db.getUsername(user).then((value) => username = value);
print(username);
if(username != null){
return Home();
}else{
_db.createBlankUser(user);
return EditProfile();
}
}
.then() is called when the value of the Future is returned. So the value of value is always non null, whereas username is null when you print it.
Try the difference by replacing .then(...) with:
.then((value){
username = value;
print(username);
});
Additionally, you can have a look at how to handle Asynchronous data in Flutter
I'm guessing _db.getUsername is returning a Future?
In that case you should look into using FutureBuilder
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
return FutureBuilder(
builder: (context, snap) {
//snap.data will be the username
if(snap.hasData) {
return Home();
} else {
//you need to wait for another Future here I guess?
return FutureBuilder(
builder: (context, snap2){
if(snap2.connectionState == ConnectionState.done){
return EditProfile();
} else {
//return some sort of circular loader icon.
}
},
future: _db.createBlankUser(user)
);
}
},
future: _db.getUsername(user),
);