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(),
),
),
};
}
Related
according to this official video from author of go_router package :
Flutter Navigator 2.0 made easy with go_router
and this Flutter Navigator 2.0: Using go_router complete guide , I implemnted go_router in my app. However, I'm having a strange problem. Everything works properly the first time I run my app. I can fetch user information from the server and send them to the home page. However, I'm getting an issue regarding fetching data after restarting my app. Would you please tell me why I'm receiving this error?
this my main.dart :
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPrefs = await SharedPreferences.getInstance();
runApp(MyApp(sharedPrefs));
}
class MyApp extends StatefulWidget {
final SharedPreferences sharedPreferences;
MyApp(this.sharedPreferences);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late LoginState loginState;
late Auth auth;
#override
void initState() {
loginState = LoginState(widget.sharedPreferences);
auth = Auth(widget.sharedPreferences);
super.initState();
}
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (ctx) => auth,
),
ChangeNotifierProvider(
create: (ctx) => loginState,
),
ChangeNotifierProvider(
create: (ctx) => AppRouter(loginState),
),
ListenableProxyProvider<Auth, Orders>(
update: (_, authObj, prevOrders) =>
Orders(authObj.usrName, authObj.objId),
),
ListenableProxyProvider<Auth, FavMeals>(
update: (_, authObj, prevOrders) =>
FavMeals(authObj.usrName, authObj.objId),
],
child: Sizer(builder: (context, orientation, deviceType) {
final _router = Provider.of<AppRouter>(context, listen: false).router;
return MaterialApp.router(
routerDelegate: _router.routerDelegate,
routeInformationParser: _router.routeInformationParser,
scaffoldMessengerKey: scaffoldMessengerKey,
theme: ThemeData(
primarySwatch: myMaterialColor),
localizationsDelegates: [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
);
}));
}
}
and here is my router.dart file :
class AppRouter with ChangeNotifier {
final LoginState authState;
AppRouter(this.authState);
late final router = GoRouter(
redirect: (state) {
final loginLoc = state.namedLocation(authScreen);
// 2
final loggingIn = state.subloc == loginLoc;
// 3
// 4
final loggedIn = authState.loggedIn;
final rootLoc = state.namedLocation(root);
// 5
if (!loggedIn && !loggingIn) return loginLoc;
if (loggedIn && (loggingIn)) return rootLoc;
return null;
},
refreshListenable: authState,
routes: [
GoRoute(
name: root,
path: '/',
builder: (context, state) => TabsScreen(),
),
GoRoute(
name: mainScreen,
path: '/main-screen',
builder: (context, state) => HomeScreen(),
),
GoRoute(
name: splashScreen,
path: '/splash-screen',
builder: (context, state) => SplashScreen(),
)
GoRoute(
name: authScreen,
path: '/auth-screen',
builder: (context, state) => AuthScreen(),
),
GoRoute(
name: orderSreen,
path: '/order-screen',
builder: (context, state) => OrderScreen(),
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
}
there is a method in my app within my '/' (root) screen that trying to fetch user data and then load the screen, this method doesnt work properly any more after I migrate to go_router :
#override
void initState() {
_selectedIndex = 0;
_pageController = PageController(initialPage: _selectedIndex!);
initUserStatus().then((_) {
setState(() {
_isloading = false;
});
}, onError: (error) {
GoRouter.of(context).goNamed(noNetwork);
print(error.toString());
});
super.initState();
}
Future<void> initUserStatus() async {
final userProvider = Provider.of<Users>(context, listen: false);
await userProvider.userRetrieving();
await Provider.of<EatenMeals>(context, listen: false).serverList();
var user = userProvider.initialUser;
dailyLimitAlert(user);
if (user.isPremium) {
var sub = await userProvider.subRemainDays();
if (sub < 1) {
await userProvider.changePremiumStatus();
}
} else {
print(user.isPremium);
}
}
So I am using this answer Prevent unauthenticated users from navigating to a route using a URL in a flutter web app? to make my task. But I always have this error: Error: Could not find the correct Provider<UserProvider> above this Consumer<UserProvider> Widget.
My main.dart:
import '/providers/user_provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
return FutureBuilder(
future: _initialization,
builder: (context, appSnapshot) {
return MaterialApp(
//smth
onUnknownRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => NotFoundErrorScreen(),
);
},
routes: {
AuthScreen.routeName: (context) => AuthScreen(),
DashboardScreen.routeName: (context) => DashboardScreen(),
ProductsScreen.routeName: (context) => ProductsScreen(),
PermissionErrorScreen.routeName: (context) => PermissionErrorScreen(),
},
builder: (context, child) {
return Consumer<UserProvider>(
child: DashboardScreen(),
builder: (context, provider, child) {
if (provider.isLoading) {
return SplashScreen();
}
final value = provider.user;
if (value == null) {
return Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
settings: settings, builder: (context) => AuthScreen()),
);
}
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserProvider()),
],
child: child,
);
},
);
},
);
},
);
}
}
My user_provider.dart:
class UserProvider with ChangeNotifier {
bool _isLoading = true;
Map<String, dynamic> _user;
bool get isLoading => _isLoading;
Map<String, dynamic> get user => _user;
getUser() async {
FirebaseAuth.instance.authStateChanges().listen((currentUser) async {
_isLoading = false;
if (currentUser == null) {
_user = null;
notifyListeners();
return;
}
final data = await FirebaseFirestore.instance
.collection('users')
.doc(currentUser.uid)
.snapshots()
.first;
if (data.exists) {
print(data.data());
if (data.data()['role'] == 'admin') {
_user = (data.data());
notifyListeners();
}
return null;
}
});
}
}
This is my first experience with flutter web, so I am confused.
My first thaught was that it does not work, because I don't invoke my getUser() function, however I do not know how to fix it.
Thank you in advance.
I have been struggling with this problem for like two days. My social media app should save its state, when signed in so that when you leave the app and come back again it should start from the home page, not the sign in page. I have found that it is possible to do this with StreamBuilder and FutureBuilder. I have tried some things with FutureBuilder and I have some errors.
Below is how my main page looks like:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => UserData(),
child: MaterialApp(
title: 'Curtain App',
debugShowCheckedModeBanner: false,
home: FutureBuilder(
future: SharedPreferencesHelper.getPrefs(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasData) {
Provider.of<UserData>(context).currentUserId =
snapshot.data.token;
return HomeScreen();
} else {
return LoginScreen();
}
},
),
),
);
}
}
class SharedPreferencesHelper {
static final String _tokenCode = "token";
static Future<String> getPrefs() async {
final SharedPreferences preferences = await SharedPreferences.getInstance();
return preferences.getString(_tokenCode) ?? "empty";
}
}
And this is my LoginPage submit btn code:
_submit() async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
// logging in the user w/ Firebase
//AuthService.login(_email, _password);
var user = await DatabaseService.loginUser(_username, _password);
final data = json.decode(user);
SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
print("Hi ${data['username']}");
print("Status ${data['status']}");
print("Token ${data['token']}");
if (data['username'] != null) {
setState(() {
_message = "Hi ${data['username']}";
sharedPreferences.setString('token', data['token']);
});
Navigator.of(context).pushAndRemoveUntil(
CupertinoPageRoute(
builder: (context) => HomeScreen(),
),
(Route<dynamic> route) => false);
}
}
}
Any ideas on how to solve this ?
Just remove the .token from the line where the error occurs. snapshot.data already is the token.
I'm trying to implement user authentication.
To log user in I created a Provider called Auth:
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../../utilities/http_exception.dart';
class Auth extends ChangeNotifier {
String _userId;
String _userUUID;
String _token;
DateTime _expiryDate;
bool get isAuth {
return token != null;
}
String get token {
if (_token != null) {
return _token;
}
return null;
}
Future<void> login(String email, String password) async {
const String url = "http://10.0.2.2:8000/users/api/token/";
try {
final response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(
{
"email": email,
"password": password,
},
),
);
final responseData = json.decode(response.body);
if (response.statusCode == 200 && responseData["token"] != null) {
_token = responseData["token"];
_userUUID = responseData["user"]["uuid"];
notifyListeners();
} else {
print("SOMETHING WENT WRONG HERE");
}
} catch (e) {
throw e;
}
}
}
As you can see I called notifyListeners() to notify listeners to do some operations.
What kind of operations?
I used a Consumer around my MateriaApp widget and for home parameter I've checked weather the token is null or not.
if it's null OK, load GeneralAuth screen otherwise load Home screen.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return OrientationBuilder(
builder: (context, orientation) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: Auth()),
ChangeNotifierProvider.value(value: Users()),
ChangeNotifierProvider.value(value: Teachers()),
],
child: Consumer<Auth>(
builder: (context, auth, _) => MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
supportedLocales: [Locale("fa", "IR")],
onGenerateRoute: Router.generateRoute,
initialRoute: GeneralAuthRoute,
onUnknownRoute: (settings) => MaterialPageRoute(
builder: (context) => Undefined(name: settings.name),
),
locale: Locale("fa", "IR"),
theme: ThemeData(
fontFamily: "IranSans",
primaryColor: Color(0xFF212121),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: Colors.black.withOpacity(0),
),
),
home: auth.isAuth ? Home() : GeneralAuth(),
),
),
);
},
);
},
);
}
}
Looks like everything is OK because as I said I called notifyListeners() but after receiving token even though I have token and auth.isAuth returns true, screen won't load up.
What is the problem?
Home property is something that is not dynamically changing your screen, and this behavior is expected. What you need to do is you need to follow through logic where you wait for login endpoint to execute, and based on response you get, you would need to decide on the screen that gets presented.
Example of that would be this:
await login('a#a.com', '12345');
if (Auth.isAuth) {
// Present here Home screen
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Home()),
);
}
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();
}
},
)
);
}
}