Flutter Firebase Authentication: delay on startup - flutter

I am using Provider and the stream FirebaseAuth.instance.onAuthStateChanged in the app to decide where to redirect on startup, but although the user is already logged in (from a previous startup) the app starts on the login screen and almost 1 second later redirects to the home page, from which it should have started from the first moment. This happens even in airplane mode.
I would like to know if there is any approach to solve this, even if it is not possible to show the home screen at once, I don't know how to differentiate between the not logged user (null->login screen) and loading user (null->loading screen).
Some of the code:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final FirebaseAuth _auth = FirebaseAuth.instance;
final DatabaseService db = DatabaseService();
#override
Widget build(BuildContext context) {
return StreamProvider<FirebaseUser>.value(
value: _auth.onAuthStateChanged,
child: Consumer<FirebaseUser>(
builder: (context, firebaseUser, child) {
return MultiProvider(
providers: [
if (firebaseUser != null)
ChangeNotifierProvider(create: (ctx) => CollectionState(firebaseUser)),
StreamProvider<List<Collection>>.value(value: db.streamCollections(firebaseUser)),
],
child: MaterialApp(
title: 'My App',
routes: {
'/': (ctx) => LandingPage(),
'/login': (ctx) => LoginPage(),
'/emailSignIn': (ctx) => EmailSignInPage(),
'/emailSignUp': (ctx) => EmailSignUpPage(),
'/emailUnverified': (ctx) => EmailUnverifiedPage(),
'/home': (ctx) => HomePage(),
'/settings': (ctx) => Settings(),
},
),
);
},
),
);
}
}
class LandingPage extends StatelessWidget {
final DatabaseService _db = DatabaseService();
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context);
final userCondition =
user == null ? 'null' : user.isEmailVerified ? 'verifiedUser' : 'unverifiedUser';
switch (userCondition) {
case 'null':
return LoginPage();
break;
case 'unverifiedUser':
return EmailUnverifiedPage();
break;
case 'verifiedUser':
return HomePage();
break;
}
}
}
The code is a bit simplified, I use a service for the authentication instance instead, just that.

I know I'm very late, but I've had the same problem for weeks and I finally figured it out.
#ChinkySight is right when he says it's best to use a StreamBuilder, mostly because you have access to the connectionState property.
The reason why lag exists is because the connection to the stream is not fully established. So during ConnectionState.waiting, return a widget like a splash screen or just a container.
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (_, snapshot) {
// Added this line
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
if (snapshot.data is FirebaseUser && snapshot.data != null) {
return HomePage();
}
return LoginPage();
});
}
}
You can even give your return statements fancy animations with the Animated Switcher
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
Widget widget;
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
switch (snapshot.hasData) {
case (true):
widget = HomePage();
break;
case (false):
widget = LoginPage();
}
return Stack(
children: <Widget>[
Scaffold(
backgroundColor: Colors.grey.shade200,
),
AnimatedSwitcher(
duration: Duration(milliseconds: 700),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
)
],
);
},
);

This works for FlutterFire.
Firebase Auth enables you to subscribe in realtime to this state via a
Stream. Once called, the stream provides an immediate event of the
user's current authentication state, and then provides subsequent
events whenever the authentication state changes. To subscribe to
these changes, call the authStateChanges() method on your FirebaseAuth
instance:
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:flutter/material.dart';
import 'menu.dart';
import 'login.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:firebase_core/firebase_core.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MyApp()
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitUp]);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TestApp',
theme: ThemeData(primarySwatch: Colors.blue),
home:
StreamBuilder<auth.User>(
stream: auth.FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<auth.User> snapshot) {
if(snapshot.hasData) {
print("data exists");
return HomePage();
}
else {
return LoginPage();
}
},
)
);
}
}

Related

Provider is not working when navigate to new screen

I implemented Authentication by provider
The problem is when is the first time myHomeCalss is notified that the user is Authenticated by dont return the correctPage (MainGui)
SplashPages is page with a button continue, and push the login page ,
The Login page is pushed outside of costumer
but when I dont pass in the SplashPages is worked perfectyl
any adea please
//splash page
ContinueButton(
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ListenableProvider.value(
value: yourModel,
child: LoginPage(),
),
),
);
}
)
//main
void main() async {
setupLocator();
WidgetsFlutterBinding.ensureInitialized();
await firebase_core.Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthenticationService()),
],
child: MyApp(),
),
);
}
//My app
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHome(),
builder: (context, child) => Navigator(
key: locator<DialogService>().dialogNavigationKey,
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => DialogManager(child: child)),
));
}
}
MyHome
Class MyHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SafeArea(
child: FutureBuilder<bool>(
future: startTime(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot2) {
if (snapshot2.hasData) {
if (snapshot2.data) {
return SplashPages();
} else {
return Consumer<AuthenticationService>(builder: (_, auth, __) {
if (auth.currentUserr == null) {
return LoginPage();
} else {
return FutureBuilder(
future: auth.populateCurrentUser(auth.currentUserr),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (auth.currentUserr.emailVerified) {
return MainGui();
} else {
return ValidationMailPage(
email: auth.currentUserr.email,
);
}
} else
return Container(
// child: Center(
// child: SpinKitRotatingCircle(
// color: Colors.white,
// size: 50.0,
// ))
);
});
}
});
}
}
You may consider using SharedPreferences, in which you will store the user (or maybe just the token), and then check in main if there is a token/user stored there before rendering the app; if there is a token you log in and then push to the homepage, if not you navigate directly to the login page.
SharedPrefenreces is persisted data storage that persists even if you restart the app, but Provider is a state management solution that doesn't persist between app restarts.
Here is the SharedPreferences plugin you may use.

How to load state on app startup using provider

I am using Provider for app state management in Flutter, Auth data is stored in shared preferences and I want to load it using provider when app starts, what is the best way to do it.
I am going to use auth status to decide whether user should see login screen or Dashboard screen
this is part of a code i used when i started flutter, implement rest of the functions as you wish.
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './providers/authentication.dart';
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => Authentication(),
),
],
child: Consumer<Authentication>(
builder: (ctx, auth, _) => MaterialApp(
title: 'MyApp',
home: auth.isAuthorized
? MyScreen()
: FutureBuilder(
future: auth.tryLogin(),
builder: (ctx, authResultSnapshot) =>
authResultSnapshot.connectionState ==
ConnectionState.waiting
? SplashScreen()
: AuthScreen(),
),
authentication.dart
String _token;
DateTime _expiryDate;
String _userId;
bool get isAuthorized {
// that is a very simple check
return token != null;
}
Future<bool> tryLogin() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey('userData')) {
return false;
}
final extractedUserData = json.decode(prefs.getString('userData')) as Map<String, Object>;
final expiryDate = DateTime.parse(extractedUserData['expiryDate']);
if (expiryDate.isBefore(DateTime.now())) {
return false;
}
_token = extractedUserData['token'];
_userId = extractedUserData['userId'];
_expiryDate = expiryDate;
notifyListeners();
return true;
}
Something like this:
Future<void> main() async {
final appState = await loadAppStateFromSharedData();
runApp(
Provider.value(
value: appState,
child: MyApp(),
),
);
}
Try this one!
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<Auth>(
builder: (ctx, auth, _) => MaterialApp(
title: 'AppName',
home: auth.isAuth
? DashboardScreen()
: AuthScreen(),
),
),
};
}

Flutter: firebase authorization flow won't work with routes

I'm trying to make sure that app-users that aren't authorised (signed in), are directed to the sign in page. This is what my main.dart looks like:
class App extends StatelessWidget {
Widget build(BuildContext context) {
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.active) {
// some loading widget
return MaterialApp(home: Scaffold(),);
}
FirebaseUser user = snapshot.data;
if (user == null) {
return MaterialApp(home: SignIn(),);
}
// this is the main app
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/new_game': (context) => NewGame(),
'/join_session': (context) => JoinSession(),
'/my_rankings': (context) => MyRankings(),
'/settings': (context) => Settings(),
},
);
}
);
}
}
When I run the app (on chrome using the web function) I start of at the sign in page (as expected) which just contains an anonymous sign in button. When I sign in it gives an error, stating that the initial-route-builder (route '/' with HomeScreen() as builder) returns null. When I swap my the main app for a simple
return MaterialApp(home: Scaffold(body: Text('This Works')));
it does seem to work. When using a simple MaterialApp() that does the same but using routes, it gives the error again, so the problem seems to be the routing. What's going on?
i don't think it will work like that. the main.dart should be used only as the app entry point.
class App extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => SplashScreen(),
'/home': (context) => HomeScreen(),
'/new_game': (context) => NewGame(),
'/join_session': (context) => JoinSession(),
'/my_rankings': (context) => MyRankings(),
'/settings': (context) => Settings(),
},
);
}
}
class SplashScreen extends StatelessWidget {
void isLogged StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.active) {
// some loading widget
Navigator.pushReplacementNamed(context, '/home');
}
FirebaseUser user = snapshot.data;
if (user == null) {
Navigator.pushReplacementNamed(context, '/SignIn');
}
// this is the main app
);
initiatState(){
isLogged();
}
Widget build(BuildContext context){
}
}
You could use this method to check if a user is logged auth.currentUser() it returns null if the user is not signed
FirebaseAuth auth = FirebaseAuth.instance;
await auth.currentUser() == null ? false : true;
Just do this!
import 'package:base_app/screens/auth/login/index.dart';
import 'package:base_app/screens/boarding/index.dart';
import 'package:base_app/screens/main/home/index.dart';
import 'package:base_app/screens/main/profile/index.dart';
import 'package:base_app/screens/splash/index.dart';
import 'package:base_app/style/palette.dart';
import 'package:base_app/style/theme.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Sizer(
builder: (context, orientation, deviceType) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
fontFamily: 'DancingScript',
textTheme: getTextTheme(context),
primarySwatch: colorRed as MaterialColor,
),
home: LandingFlow(),
routes: {
ProfileScreen.id: (context) => ProfileScreen(),
},
);
},
);
}
}
class LandingFlow extends StatefulWidget {
#override
_LandingFlowState createState() => _LandingFlowState();
}
class _LandingFlowState extends State<LandingFlow> {
bool isSplashOver = false;
bool hasBoardingScreensShown = true;
#override
Widget build(BuildContext context) {
if (isSplashOver) {
if (hasBoardingScreensShown) {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasData) {
debugPrint('HomeScreen');
return HomeScreen();
} else {
debugPrint('LoginScreen');
return LoginScreen();
}
},
);
}
return BoardingScreen();
}
return SplashScreen(
onFinished: () => setState(() {
isSplashOver = true;
}),
);
}
}
And for your Splash Screen:
import 'package:flutter/material.dart';
class SplashScreen extends StatelessWidget {
static const id = 'SplashScreen';
final Function onFinished;
const SplashScreen({this.onFinished});
#override
Widget build(BuildContext context) {
Future.delayed(
const Duration(seconds: 3),
() => onFinished(),
);
return const Scaffold(
body: Center(
child: Text('Splash Screen'),
),
);
}
}

ConnectionState changes twice in main class

I have this app where I have an intro screen that I want to hide if the user has already skipped once.
I'm using bloc with a Provider.
My issue is the connection state changes twice when I hot restart the app and I've been spending hours without understanding the reason.
Here is my code:
my main class
void main() => runApp(StatsApp());
class StatsApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => IntroProvider(),
)
],
child: Consumer<IntroProvider>(builder: (context, value, child) {
return MaterialApp(
home: FutureBuilder(
future: value.bloc.checkSkipped(),
builder: (BuildContext context, snapshot) {
print(snapshot.connectionState);
print(snapshot.data);
return SplashScreen();
},
));
}),
);
}
}
my bloc
enum IntroEvents {
ReadLocalStorage,
SetIntroSkipped,
}
class IntroBloc extends Bloc<IntroEvents, bool> {
PrefsManager _prefsManager = PrefsManager.instance;
Future<bool> checkSkipped() async {
this.add(IntroEvents.ReadLocalStorage);
final skipped =
await _prefsManager.getValue(PrefTypes.Bool, "skippedIntro");
return skipped;
}
#override
// TODO: implement initialState
bool get initialState => false;
#override
Stream<bool> mapEventToState(IntroEvents event) async* {
switch (event) {
case IntroEvents.SetIntroSkipped:
_prefsManager.setValue(PrefTypes.Bool, "skippedIntro", true);
yield true;
break;
case IntroEvents.ReadLocalStorage:
final skipped =
await _prefsManager.getValue(PrefTypes.Bool, "skippedIntro");
yield skipped;
break;
default:
print("wtffffff");
}
}
}
my provider
class IntroProvider with ChangeNotifier {
IntroBloc _bloc;
IntroProvider(){
print("called IntroProvider");
_bloc = IntroBloc();
}
IntroBloc get bloc => _bloc;
}
Any help would be highly appreciated.
When working with snapshots you're able to check whether the snapshot.hasData. Normally you'd wrap the functionality you want to run when the snapshot has data in an if statement and provide some kind of default Widget when it does not.
FutureBuilder(
future: value.bloc.checkSkipped(),
builder: (BuildContext context, snapshot) {
print(snapshot.connectionState);
if (snapshot.hasData) {
print(snapshot.data);
}
return SplashScreen();
},
);

FLUTTER - Futurebuilder keeps returning null after Location Permission

The Problem
Futurebuilder keeps returning "null" after the user has given permission to acces it's location so it can calculate the distance between 2 locations.
What I want it to do
It does give the location when the page is refreshed but I want the distance between 2 objects when the user gives acces to it's location, not when the user refreshed their page.
The Main Code to run the app
import 'package:flutter/material.dart';
import 'mainlist.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
routes: {
'/second': (context) => mainlist()
},
title: "testapp",
debugShowCheckedModeBanner: false,
home: mainlist(),
);
}
}
The code where the problem happens - Futurebuilder + getCurrenPosition Future
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:async/async.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission/permission.dart';
import 'mainlist.dart';
class mainlist extends StatefulWidget {
#override
_mainlistpage createState() => _mainlistpage();
}
class _mainlistpage extends State<mainlist> {
Future<String> getCurrentPosition(DocumentSnapshot document) async{
Position position = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
double distanceInMeters = await Geolocator().distanceBetween(position.latitude, position.longitude, document['lat'], document['lat']);
return distanceInMeters.toString();
}
var sortBy = "";
Widget homePage() {
return StreamBuilder(
stream: Firestore.instance.collection("Test").orderBy(sortBy).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Text("Loading");
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) =>
_mainListItem(context, snapshot.data.documents[index]));
},
);
}
#override
Widget _mainListItem(BuildContext context, DocumentSnapshot document) {
return Scaffold(
body: Container(
child: Center(
child: Column(
children: <Widget>[
FutureBuilder(
future: getCurrentPosition(document),
builder: (BuildContext context,AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('none');
case ConnectionState.active:
case ConnectionState.waiting:
return Text('waiting');
case ConnectionState.done:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
return Text(snapshot.data.toString());
}
return null; // unreachable
}
),
]
),
),
),
);
}
Widget build(BuildContext context){
return new Scaffold();
}
}
What have I tried
Using streambuilder
Messing with the cases in the Futurebuilder
Reading Stackoverflow
I have added the permissions in plist and manifest