Retrigger FutureBuilder on Login / Registration - flutter

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...

Related

How to navigate to the Home Page after a successful Login using Flutter (Dart)

I have a Login Controller that does the action of loging in (with Google)
class LoginController extends GetxController {
final _googleSignin = GoogleSignIn();
var googleAccount = Rx<GoogleSignInAccount?>(null);
login() async {
googleAccount.value = await _googleSignin.signIn();
}
logout() async {
googleAccount.value = await _googleSignin.signOut();
}
}
Then I have a Log In UI that shows everything
final controller = Get.put(LoginController());
LoginIn({
super.key,
required String title,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sign-In Page')),
body: Center(
child: Obx(() {
if (controller.googleAccount.value == null) {
return loginButton(); //the Login button to log in with google
} else {
return seeProfile(); //after Login you are then able to see your profile
}
}),
));
}
And then ofcourse I have a HomePage
How do I go to the Home Page after a successful log in
I tried to use ChatGPT and I did watch a couple of Youtube Videos but their code is ofcourse different then my and that makes it hard to implement their solution
A cleaner method would be to use Stream builder and subscribe to the auth stream in the main.dart file. Based on the value, navigate the user.
'''
StreamBuilder<User?>(
initialData: null,
// stream: RepositoryProvider.of(context).authStream(),
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasData) {
return HomePage();
}
return Container();
});
I think checking the example in the google_sign_in's package on pub.dev might help you achieving what you want.
In summary, I would suggest you to listen to changes to the user in the initState method and push a new page after signIn is successful:
#override
void initState() {
super.initState();
StreamSubscription<GoogleSignInAccount?> subscription = _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) {
if (account != null) {
//push new screen
Navigator.of(context).popAndPushNamed(...)
subscription.cancel()
}
});
}
I couldn't test this code, but it should give you a general idea on how to proceed. Again, check the link above for more information.

StreamBuilder snapshot is inactive on creating own stream

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!

Flutter secure routes with a flutter-fire authentication guard and avoid unnecessary rebuilds

I currently face an issue where I route to an authentication guard view as my default route.
My authentication guard view:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/user.dart';
import '../services/services.module.dart';
import '../widgets/common/async_stream.dart';
import 'landing_screen/landing_screen.dart';
import 'tabs_screen/tab_screen.dart';
/// The [ViewAuthGuard] decides whether to display the [LandingScreenView] or the [TabsScreenView].
class ViewAuthGuard extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('ViewAuthGuard build called: $context');
FirebaseAuthService authService = Provider.of<AuthService>(context, listen: false);
return AsyncStreamWidget<User>(
stream: authService.onAuthStateChanged,
child: (User user) => TabsScreenView(),
emptyWidget: LandingScreenView(),
loadingWidget: null,
errorWidget: null,
);
}
}
and my AsyncStreamWidget:
import 'package:flutter/material.dart';
import '../../../models/base_model.dart';
import '../../error/future_error.dart';
import '../../loading.dart';
class AsyncStreamWidget<T extends BaseModel> extends StatelessWidget {
final Stream<T> stream;
final T initialData;
Widget _loading;
Widget _empty;
Widget Function(Object) _error;
Widget Function(T) child;
AsyncStreamWidget({
#required this.stream,
#required this.child,
this.initialData,
Widget loadingWidget,
Widget emptyWidget,
Widget Function(Object) errorWidget,
}) {
if (loadingWidget == null) {
_loading = Loading();
} else {
_loading = loadingWidget;
}
if (errorWidget == null) {
_error = (Object error) => FutureErrorWidget(error: error);
} else {
_error = errorWidget;
}
if (emptyWidget == null) {
_empty = Center(child: Text('No data available.'));
} else {
_empty = emptyWidget;
}
}
#override
Widget build(BuildContext context) {
return StreamBuilder<T>(
initialData: initialData,
stream: stream,
builder: (_, AsyncSnapshot<T> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return _loading;
break;
case ConnectionState.active: // check if different behavior is needed for active and done
case ConnectionState.done:
// error state
if (snapshot.hasError) {
// todo more throughout error checking and specialized error widget
return _error(snapshot.error);
}
// data state
if (snapshot.hasData) {
T data = snapshot.data;
return child(data);
}
// empty state
return _empty;
case ConnectionState.none:
default:
print('E: Received Future [$stream] was null or else.');
return _error('Unknown error.');
}
},
);
}
}
The FirebaseAuthService wraps the auth.FirebaseAuth.instance. My stream is constructed as follows:
User _userFromFirebase(auth.User user) {
if (user == null) {
return null;
}
return User(
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoURL,
);
}
#override
Stream<User> get onAuthStateChanged => _firebaseAuth.authStateChanges().map(_userFromFirebase);
I currently provide all my services above the ViewAuthGuard.
I wrapped my Material app with a ThemeProvider ChangeNotifier (in case that could be an issue).
My issue is that all widgets below the ViewAuthGuard are rebuild and their state is reset. This occurs to me while developing. When a hot reload occurs, all the children are rebuild. The TabsScreenView contains the initial navigation for my flutter app and always reset to index zero during development.
Question: How do I avoid the unnecessary reloads at this point?
What I tested so far:
I wrapped my named route for TabsScreenView with FutureBuilder / StreamBuilder and set it as the default route (Route Guards in Flutter)
Listening to the stream in didComponentUpdate and pushing named routes on User change
The solution provided above
Please drop me a comment if you need more information, code, console prints or else to support me. Thank you!
I was able to fix it myself. For anyone interested, this was the process:
The stream is the pivot element which decides if the AsyncStreamWidget rebuilds its child or not. So I did check if the stream changed across hot reloads by printing its hashCode attribute (and yes, it did change).
Subsequently, I did change my ViewAuthGuard to a StatefulWidget and used the didChangeDependencies method to store the stream in the state.
(I also started instantiating the widgets in the initState method and store them in the state as well so that they are created only once.)
EDIT And I should mention that my ViewAuthGuard has no dependencies. So it is not rebuilt due to a change of dependencies.

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.

Email verif status will not become true except in the vscode i do hot reload

I have problem with email verif status. I want if email verif status is true then go to home. I have this if condition in RaisedButton, so if user click the button and email verif status is true, then return to Wrapper.
dynamic verifEmail = await _auth.isEmailVerified();
if (verifEmail == true) {
print('in');
// Navigator.of(context).pushNamed(Home.tag);
return Wrapper();
}
this is the code of isEmailVerified()
Future<bool> isEmailVerified() async {
FirebaseUser user = await _auth.currentUser();
await user.reload();
user = await _auth.currentUser();
return user.isEmailVerified;
}
and this is the code of the Wrapper class.
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return Scaffold(
body :StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
print('Verif email ' + snapshot.data.isEmailVerified.toString());
return snapshot.data.isEmailVerified
? Home(uidCode: user.uid)
: VerifEmail(user: snapshot.data);
}
else {
return Authenticate();
}
},
),
);
}
}
The problem is in the Wrapper class, email verif status will not become true except in the vscode i do hot reload. So i cant go to home page except i do hot reload in the vscode. Please anyone can fix this problem ?
I don't know where the problem is, but if you need to do a Hot Reload, adding a setState((){}) call somewhere may fix the issue.