FutureBuilder in flutter building app again and again? - flutter

what I am trying to do is check if user is logged in already or not. there is a futurebuilder inside consumer which is notifying listeners. check code:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: Auth(),
)
],
child: Consumer<Auth>(
builder: (ctx, auth, _) {
print('making it again and again');
return MaterialApp(
title: 'MY app',
theme: ThemeData(
primarySwatch: Colors.blue,
accentColor: Colors.deepOrange,
),
home: auth.isAuth
? FirstScreen()
: FutureBuilder(
future: auth.tryAutoLogin(), //inside this function there is notifylisteners()
builder: (ctx, authResultSnapShot) =>
authResultSnapShot.connectionState ==
ConnectionState.waiting
? SplashScreen()
: AuthScreen(),
));
},
),
);
}
What I get:
this is rebuilding the app again and again. tryautologin is called repeatedly.
What I want:
check for is a user logged in only once the app is started.
Future<bool> tryAutoLogin() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey('userData')) {
return false;
}
final extractedUserData = json.decode(prefs.getString('userData'));
final expiryDate = DateTime.parse(extractedUserData['expiryDate']);
_accessToken = extractedUserData['accessToken'];
_refreshToken = extractedUserData['refreshToken'];
print(extractedUserData['userId']);
_userId = extractedUserData['userId'];
if (expiryDate.isBefore(DateTime.now())) { //for now this if statement is not running
try {
await _getAccessToken();
} catch (error) {
return false;
}
}
print('tryautologin');
notifyListeners();
_autoGetAccessToken(); //this function doesn't have any notifylisteners
return true;
}
edited:
String get accessToken {
if (_expiryDate != null &&
_expiryDate.isAfter(DateTime.now()) &&
_accessToken != null) {
return _accessToken;
}
return null;
}
bool get isAuth {
print(accessToken != null);
return accessToken != null;
}

Even the question is old quite a bit of time, this answer will help another. I have faced the problem also. this happens when you use a future builder inside a consumer. it is rebuilding again and again infinitely. So, instead of the future builder try using an if-else statement.
child: Consumer<Auth>(
builder: (ctx, auth, _) {
//Sizedbox() used for nullsafety
Widget child = SizedBox();
if (auth.isAuth) {
child = FirstScreen();
} else if (!auth.triedAutoLogin) {
auth.tryAutoLogin();
child = SplashScreen();
} else {
child = AuthScreen();
}
return MaterialApp(
title: 'MY app',
theme: ThemeData(
primarySwatch: Colors.blue,
accentColor: Colors.deepOrange,
),
home: child,
);
},
),
inside Auth class keep a boolean variable like triedAutoLogin to keep track.

Related

Flutter: How to remove all previous routes when auth steam change state?

I would like to remove all previous routes and return to #WelcomeScreen
in case API response 401
Code
return GetMaterialApp(
scrollBehavior: Behavior(),
defaultTransition: Transition.leftToRight,
translations: LanguageService(),
locale: Get.locale,
fallbackLocale: const Locale('en', 'US'),
debugShowCheckedModeBanner: false,
home: Obx(
() {
if (controller.state is Authenticated) {
return const MainScreen();
} else if (controller.state is UnAuthenticated) {
return WelcomeScreen();
} else if (controller.state is AuthSignIn) {
return SignInScreen();
} else if (controller.state is AuthSignUp) {
return SignUpScreen();
} else {
return const SplashScreen();
}
},
),
theme: AppTheme.light,
darkTheme: AppTheme.dark,
getPages: AppPages.list);
Controller
AuthController auth = Get.find();
Future<void> fetchUsers() async{
var response = await userService.findAll();
//......
if(response.code==401){
auth.authStateStream.value = UnAuthenticated();
Get.back();
Get.back();
}
}
Currently, on state change to UnAuthenticated, it returns to WelcomeScreen
but it does not remove some previous pages. I need to use Get.back one or more depending on the pages I have pushed.
Is there any better solution for this, please give me a suggestion or advice.
Thank you!
You can do :
Get.offAll(()=>WelcomeScreen());

Flutter + Hive Check if value is present in box in Future Builder

I am crafting a new app and saving the response from a REST API call to a Hive box, which is successful (as far as I know). What I am trying to do in my main.dart is check if the value for token is set in Hive and if it is load an alternative view other than the login view.
My main.dart
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:myapp/model/user_model.dart';
import 'package:myapp/views/login_view.dart';
import 'package:myapp/views/project_view.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocsDir = await path_provider.getApplicationDocumentsDirectory();
Hive.init(appDocsDir.path);
Hive.registerAdapter(UserModelAdapter());
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<PocketPolarix> createState() => _PocketPolarixState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
final user = Hive.box('user');
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2036B8),
foregroundColor: Colors.white,
),
),
home: FutureBuilder(
future: Hive.openBox('user'),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
Hive.openBox('user');
if (user.get('token') != null) {
return const ProjectView();
} else {
return const LoginView();
}
}
} else {
return Scaffold();
}
},
),
);
}
#override
void dispose() {
Hive.close();
super.dispose();
}
The code is failing at the second if statement where I check to see if Hive.box('user').get('token') != null what I keep getting is the following error.
throw HiveError('Box not found. Did you forget to call Hive.openBox()?'); as you can see from the code however, I am opening the box.
I am new to Dart and Flutter and Hive for that matter so a helping hand here would be great, thanks!
Part 1 of the issue is that you were trying to access user before opening. Secondly, I believe you need to check if the snapshot.hasData before proceeding with any operation i.e.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2036B8),
foregroundColor: Colors.white,
),
),
home: FutureBuilder(
future: Hive.openBox<YourUserModelBox>('user'),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else if (snapshot.hasData) {
if (snapshot.data is YourUserModelBox) {
final user = snapshot.data as YourUserModelBox;
if (user.get('token') != null) {
return const ProjectView();
} else {
return const LoginView();
}
} else {
Scaffold();
}
}
} else {
return Scaffold();
}
},
),
);
}
The problem is with the first line of code in your build function. You're trying to get the user box from Hive without opening it first. Here is what can you do instead:
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2036B8),
foregroundColor: Colors.white,
),
),
home: FutureBuilder<Box>(
future: Hive.openBox('user'),
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
if (snapshot.data?.get('token') != null) {
return const ProjectView();
} else {
return const LoginView();
}
}
} else {
return Scaffold();
}
},
),
);
}

Flutter MaterialApp home based on futureBuilder

When my app starts up, it detects whether the user is logged in or not (with firebase) and based on this check, it shows the homepage or the login page. Up to now everything is fine, but I would like to add one more layer.
The user can login as normal user or as admin.
So the check should be not only on the authentication state, but also on the "level" of the user, and show different pages, based on the user level.
I get the user level with a query on the firestore database, so it's a Future.
This is the code i'm using:
final usersCollection = FirebaseFirestore.instance.collection('users');
User loggedUser = FirebaseAuth.instance.currentUser;
Future<InfoUtente> userInfo;
String livelloLogin;
// here I get the user from the firestore database, based on the authenticated user id
Future<InfoUtente> fetchInfoUtente() async {
final response = await usersCollection
.where(
'uid',
isEqualTo: loggedUser.uid,
)
.get();
return InfoUtente.fromFireStore(response.docs.first);
}
// here I return the page based on the user authentication "level"
Future<Widget> widgetChoice() async {
if (!isLogged)
return LoginNewPage();
else {
userInfo.then(
(value) {
livelloLogin = value.loginLevel;
if (livelloLogin == 'struttura')
return StrutturaPage();
else
return MainPage();
},
);
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// the homepage of the material app is a future builder
home: FutureBuilder(
future: widgetChoice(),
builder: (BuildContext context, AsyncSnapshot<Widget> widget) {
if (!widget.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return widget.data;
},
),
);
}
something is not right because it always shows the circular progress indicator.
What am I doing wrong?
Is this the correct way of doing this or am I completely wrong?
If there is no data fetched or found, your screen will stuck on loading infinitely. Update your builder's implementation as
builder: (BuildContext context, AsyncSnapshot<Widget> widget) {
if(widget.connectionState == ConnectionState.done){
if (!widget.hasData) {
return Center(
child: Text('No Data exists')
);
}
return widget.data;
}
return Center(
child: CircularProgressIndicator(),
);
},
And update your widgetChoice as
Future<Widget> widgetChoice() async {
if (!isLogged)
return LoginNewPage();
else {
var userInfo = await fetchInfoUtente();
livelloLogin = userInfo.loginLevel;
if (livelloLogin == 'struttura')
return StrutturaPage();
else
return MainPage();
}
}
If i'm right you have to call the future function like that:
FutureBuilder(
future: widgetChoice,
Without ()

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

Navigate to Home screen after receiving token in Flutter

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