Run Function / Get Data when loading new screen in Flutter - flutter

I am trying to run a function that gets data via an API call that needs to initialize before / while loading the screen. I have tried using a future builder, and other methods, and perhaps i am not implementing this correctly but i cannot figure out how to do this. I have also called an async method that uses setState within my initstate method which works, but it initially loads and returns an error and what I assume is that after the state rebuilds it displays correctly.Ideally this should get the data before actually loading the screen to avoid errors.
Code for my main function in the main.dart file
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]
);
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
final SharedPreferences prefs = await _prefs;
late String initialRoute;
var accessToken = prefs.getString('access_token');
var refreshToken = prefs.getString('refresh_token');
if (accessToken != null && refreshToken != null) {
var response = await sessionRefresh(accessToken);
if (response.statusCode == 200) {
initialRoute = kDashboardID;
}
else if (response.statusCode == 401) {
var refreshResponse = await tokenRefresh(refreshToken);
if (refreshResponse.statusCode == 200) {
prefs.setString('access_token', jsonDecode(refreshResponse.body)["access_token"]);
initialRoute = kDashboardID;
}
else {
initialRoute = kLoginScreenID;
}
}
}
else {
initialRoute = kLoginScreenID;
}
runApp(MyApp(initialRoute: initialRoute,));
}
class MyApp extends StatelessWidget {
//const MyApp({Key? key}) : super(key: key);
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
String initialRoute;
MyApp({required this.initialRoute});
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => UserData(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
kLaunchScreenID: (context) => LaunchScreen(),
kLoginScreenID: (context) => LoginScreen(),
kUserRegistrationID: (context) => UserRegistration(),
kAccountVerificationID: (context) => OTPVerification(),
kDashboardID: (context) => Dashboard(),
kAccountsID: (context) => Accounts(),
},
initialRoute: initialRoute,
),
);
}
}
The ideal result would be when the initialRoute equals "kDashboardID" (user dashboard) i need to run a function that gets data so that the data can be used to populate certain widgets (text widgets, etc.) on the dashboard screen.
Not sure how to best to accomplish this.

Related

ChangeNotifierProvider does not update the model

i am quite new with flutter. I am trying to add a ChangeNotifierProvider into my app. I use flutter_azure_b2c to log in a user, in order to handle to login outcome I have the following code:
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedPayload = token.token.split('.')[1];
final payloadData =
utf8.fuse(base64).decode(base64.normalize(encodedPayload));
final claims = Claims.fromJson(jsonDecode(payloadData));
var m = Provider.of<LoginModel>(context);
m.logIn(claims);
}
}
});
The problem is that when it arrives to var m = Provider.of<LoginModel>(context); the execution stops with out errors without executing m.logIn(claims);, so the model is not changed and the consumer is not called.
Any idea?
This is my consumer:
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LoginModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: appTheme,
home: Consumer<LoginModel>(
builder: (context, value, child) =>
value.claims != null ? const Home() : const Login(),
)),
);
}
}
class LoginModel extends ChangeNotifier {
Claims? _claims;
logIn(Claims claims) {
_claims = claims;
notifyListeners();
}
logOut() {
_claims = null;
notifyListeners();
}
Claims? get claims => _claims;
}
My LoginWidget:
class Login extends StatefulWidget {
const Login({super.key});
#override
LoginState createState() => LoginState();
}
class LoginState extends State<Login> {
B2CConfiguration? _configuration;
checkLogin(BuildContext context) async {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedData = token.token.split('.')[1];
final data =
utf8.fuse(base64).decode(base64.normalize(encodedData));
final claims = Claims.fromJson(jsonDecode(data));
var m = Provider.of<LoginModel>(context, listen: true);
m.logIn(claims); //<-- debugger never reaches this line
}
}
#override
Widget build(BuildContext context) {
// It is possible to register callbacks in order to handle return values
// from asynchronous calls to the plugin
AzureB2C.registerCallback(B2COperationSource.INIT, (result) async {
if (result.reason == B2COperationState.SUCCESS) {
_configuration = await AzureB2C.getConfiguration();
if (!mounted) return;
await checkLogin(context);
}
});
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
if (!mounted) return;
await checkLogin(context);
}
});
// Important: Remeber to handle redirect states (if you want to support
// the web platform with redirect method) and init the AzureB2C plugin
// before the material app starts.
AzureB2C.handleRedirectFuture().then((_) => AzureB2C.init("auth_config"));
const String assetName = 'assets/images/logo.svg';
final Widget logo = SvgPicture.asset(
assetName,
);
return SafeArea(
child: //omitted,
);
}
}
I opened an issue as well, but it did not help me.
Try this
var m = Provider.of<LoginModel>(context, listen: false)._claims;
You are using the Provider syntax but not doing anything really with it. You need to set it like this Provider.of<LoginModel>(context, listen: false).login(claims) and call it like this Provider.of<LoginModel>(context, listen: false)._claims;
I fixed it, moving the callback registrations from the build method to the initState method.

How to store and pass SharedPreference value to other pages in flutter?

When user logs into the app I need to set 'PWD' in the shared_preference variable. I need to get that value in splashcreen of my app so that when user opens the app again it need redirect to only password entering page. How can I do it in flutter.
onPressed: () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
appdata.loginmode = prefs.setString('LOGIN_MODE', 'PWD');
Navigator.push(
context,
MaterialPageRoute(builder: (context) => BottomNavigation()),
);
print('Shared....');
print(prefs.getString('LOGIN_MODE'));
},
This what I am doing when user click login it will set to 'PWD', then I need to call the prefs in splashscree.
Short Answer
Not for splash screen but I am using the same logic for the onboard screen. I hope this answer will help. So, on your main.dart file, create a nullable int onBoardCount, outside of any class, you're gonna need this on your splash screen. Also, instantiate SharedPreferences in main and pass it with onboardcount to you MyApp();
int? onBoardCount;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
// Get onboard count from prefs, if it already exists,if not it will return null
onBoardCount = prefs.getInt('onBoardKey');
runApp(MyApp(prefs,onBoardCount));
}
Now, your MyApp file should be something like
class MyApp extends StatefulWidget {
late SharedPreferences prefs;
....
MyApp(this.prefs,this.onBoardCount, ...);
Now in your splash_screen.dart use the following logic.
void onSubmitDone(AppStateProvider stateProvider, BuildContext context) {
await prefs.setInt('onBoardKey', 0);
// Some route logic like route.push("/home");
}
Long Answer
I am using Go Router for routing and Provider for state management so, here's my app's code.
Main.dart
int? onBoardCount;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
onBoardCount = prefs.getInt('onBoardKey');
....
runApp(MyApp(prefs, onBoardCount));
}
I have a separate MyApp file to reduce congestion.
my_app.dart
class MyApp extends StatefulWidget {
late SharedPreferences prefs;
int? onBoardCount;
MyApp(this.prefs, this.onBoardCount,..... {Key? key})
: super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// The appstate provider is handling app level state
late AppStateProvider appStateProvider;
#override
void didChangeDependencies() {
super.didChangeDependencies();
appStateProvider = AppStateProvider(
widget.onBoardCount, widget.prefs,....);
}
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
....
ChangeNotifierProvider(
create: (context) => AppStateProvider(
widget.onBoardCount,
widget.prefs,...)),
Provider(
create: (context) => AppRouter(
appStateProvider: appStateProvider,
onBoardCount: widget.onBoardCount,
prefs: widget.prefs,
),
),
],
child: Builder(
builder: ((context) {
final GoRouter router = Provider.of<AppRouter>(context).router;
return MaterialApp.router(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate);
}),
),
);
}
}
App State Provider File
Create a function to update onboard logic and notify listeners.
class AppStateProvider with ChangeNotifier {
AppStateProvider(this.onBoardCount, this.prefs,..);
int? onBoardCount;
late SharedPreferences prefs;
bool? _isOnboarded;
bool get isOnboard => _isOnboarded as bool;
void hasOnBoarded() async {
await prefs.setInt('onBoardKey', 0);
_isOnboarded = true;
notifyListeners();
}
}
On Router file
class AppRouter {
late AppStateProvider appStateProvider;
late SharedPreferences prefs;
int? onBoardCount;
AppRouter({
required this.appStateProvider,
required this.onBoardCount,
required this.prefs,
});
get router => _router;
late final _router = GoRouter(
refreshListenable: appStateProvider,
initialLocation: "/",
routes: [
...
],
redirect: (state) {
final String onboardLocation =
state.namedLocation("Your Route name");
bool isOnboarding = state.subloc == onboardLocation;
bool? toOnboard = prefs.containsKey('onBoardKey') ? false : true;
print("Is LoggedIn is $isLoggedIn");
if (toOnboard) {
return isOnboarding ? null : onboardLocation;
}
return null;
});
}
Since the router is listening to appStateProvider, it will change once you call hasOnBoarded() on your onboard screen.
OnBoardScreen
void onSubmitDone(AppStateProvider stateProvider, BuildContext context) {
stateProvider.hasOnBoarded();
GoRouter.of(context).go("/");
}
I hope this will help please leave comments. FYI, ... is some other codes that I feel it's not important for this topic.

How to redirect to a login page if Flutter API response is unauthorized?

I am building a Flutter app which uses a Golang API to fetch data. The API will return a 401 unauthorized if the JWT token is not valid. How can I redirect to a login page on any API call if the response status is 401?
Here is my flutter code:
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Provider.debugCheckInvalidValueType = null;
AppLanguage appLanguage = AppLanguage();
await appLanguage.fetchLocale();
runApp(MyApp(
appLanguage: appLanguage,
));
}
class MyApp extends StatelessWidget {
final AppLanguage appLanguage;
MyApp({this.appLanguage});
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: providers,
child: MaterialApp(
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
initialRoute: RoutePaths.Login,
onGenerateRoute: Router.generateRoute,
)
);
}
}
tables.dart
class Tables extends StatelessWidget {
const Tables({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BaseWidget<TablesModel>(
model: TablesModel(api: Provider.of(context, listen: false)),
onModelReady: (model) => model.fetchTables(),
builder: (context, model, child) => model.busy
? Center(
child: CircularProgressIndicator(),
)
: Expanded(
child: GridView.builder (
---
tables_model.dart
class TablesModel extends BaseModel {
Api _api;
TablesModel({#required Api api}) : _api = api;
List<Tbl> tables;
Future fetchTables() async {
setBusy(true);
tables = await _api.getTables();
setBusy(false);
}
#override
void dispose() {
print('Tables has been disposed!!');
super.dispose();
}
}
api.dart
Future<List<Tbl>> getTables() async {
var tables = List<Tbl>();
try {
var response = await http.get('$_baseUrl/tables/list');
var parsed = json.decode(response.body) as List<dynamic>;
if (parsed != null) {
for (var table in parsed) {
tables.add(Tbl.fromJson(table));
}
}
} catch (e) {print(e); return null;}
return tables;
}
Since you already have a MaterialApp in your tree and the named routes registered, this should be as simple as adding a call to push your login page around the same time you get the response.
First, you should modify getTables to check response for the status code with statusCode property of the Response object and shown with the following code block:
var response = await http.get('$_baseUrl/tables/list');
if(response.statusCode == 401) {
//Act on status of 401 here
}
Now that you have a way of checking when the response has a status code of 401, you can navigate to your login page with the Navigator. The Navigator needs BuildContext, so that must be passed to the getTables function.
This involves modifying getTables to be:
Future<List<Tbl>> getTables(BuildContext context) async {
and fetchTables needs a similar change:
Future fetchTables(BuildContext context) async {
Then, where these methods are called, you pass context down:
In Tables
model.fetchTables(context)
In TablesModel
Future fetchTables(BuildContext context) async {
setBusy(true);
tables = await _api.getTables(context);
setBusy(false);
}
and finally in getTables, you use the passed context to use the Navigator:
Future<List<Tbl>> getTables(BuildContext context) async {
var tables = List<Tbl>();
try {
var response = await http.get('$_baseUrl/tables/list');
//Check response status code
if(response.statusCode == 401) {
Navigator.of(context).pushNamed(RoutePaths.Login);//Navigator is used here to go to login only with 401 status code
return null;
}
var parsed = json.decode(response.body) as List<dynamic>;
if (parsed != null) {
for (var table in parsed) {
tables.add(Tbl.fromJson(table));
}
}
} catch (e) {print(e); return null;}
return tables;
}
Instead of Navigator.of(context).pushNamed(RoutePaths.Login);, you could use Navigator.pushNamed(context, RoutePaths.Login); if you prefer, but as you can read at this answer, they internally do the same thing.
Now when there is a status code of 401, a user will be navigated to the login screen.

initialRoute string is changed, but I end up at the same page regardless the initialRoute string

When using shared_preferences on flutter in main.dart in order to change the initialRoute depending on if user have seen the first page or if user is logged in I am getting the boolean which is created throughout the app and added to shared_preferences, every time I start app, I get the initialRoute string correct when debugging, but I still end up getting on the first page, regardless the conditions.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:developer';
import './pages/registration.dart';
import './pages/login_page.dart';
import './pages/confirmation.dart';
import './pages/lang_page.dart';
import './pages/main_page.dart';
import './pages/user_data.dart';
import './provider/provider.dart';
void main() => runApp(CallInfoApp());
class CallInfoApp extends StatefulWidget {
#override
_CallInfoAppState createState() => _CallInfoAppState();
}
class _CallInfoAppState extends State<CallInfoApp> {
SharedPreferences prefs;
void getSPInstance() async {
prefs = await SharedPreferences.getInstance();
}
dynamic langChosen;
dynamic isLoggedIn;
String initialRoute;
void dataGetter() async {
await getSPInstance();
setState(() {
langChosen = prefs.getBool('langChosen');
// print(langChosen);
isLoggedIn = prefs.getBool('isLoggedIn');
});
}
void getRoute() async {
await dataGetter();
debugger();
if (langChosen == true && isLoggedIn != true) {
setState(() {
initialRoute = '/login_page';
});
} else if (isLoggedIn == true) {
initialRoute = '/main_page';
} else {
setState(() {
initialRoute = '/';
});
}
}
#override
void initState() {
super.initState();
debugger();
getRoute();
}
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
return ChangeNotifierProvider<AppData>(
create: (context) => AppData(),
child: MaterialApp(
title: 'Call-INFO',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: initialRoute,
routes: {
'/': (context) => LanguagePage(),
'/registration_page': (context) => RegistrationPage(),
'/login_page': (context) => LoginPage(),
'/confirmation_page': (context) => ConfirmationPage(),
'/user_data_page': (context) => UserDataPage(),
'/main_page': (context) => MainPage(),
},
),
);
}
}
Since SharedPreference.getInstance() is an async function it will need some time until the instance is available. If you want to use it for initial route you have to make your main function async and preload it there before your MaterialApp is build.
SharedPreference prefs; //make global variable, not best practice
void main() async {
prefs = await SharedPreference.getInstance();
runApp(CallInfoApp());
}
And remove getSPInstance() from dataGetter
Also keep in midn that prefs.getBool('langChosen') will return null and not false if no entry is made into shared preference so use
langChosen = prefs.getBool('langChosen')??false;
isLoggedIn = prefs.getBool('isLoggedIn')??false;
While this solution will work it's not really good practice. I would recommend to have the initialRoute fixed to a splash screen and handle forwarding to the right page from there. A simple splash screen could look like that:
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
#override
void initState() {
initSplash();
super.initState();
}
Future<void> initSplash() async {
final prefs = await SharedPreferences.getInstance();
final langChosen = prefs.getBool("lang_chosen") ?? false;
final isLoggedIn = prefs.getBool("logged_in") ?? false;
if (langChosen == true && isLoggedIn != true) {
Navigator.of(context).pushReplacementNamed('/login_page');
} else if (isLoggedIn == true) {
Navigator.of(context).pushReplacementNamed('/main_page');
} else {
Navigator.of(context).pushReplacementNamed('/');
}
}
}
Use initState to derive the data your logic is based on (i.e. fetching shared pref info). And use await keyword so that program will wait until the data is fetched from SharedPrefs. Adding the following code to class _CallInfoAppState should help
#override
void initState() {
super.initState();
dataGetter();
}
void dataGetter() async {
await getSPInstance();
setState(() {
langChosen = prefs.getBool('langChosen');
isLoggedIn = prefs.getBool('isLoggedIn');
});
}

How can i set loading screen when apps check login status?

I use SharedPreferences to keep login status. its works fine.
but after close my app when I open this. it's showing all my screen like flash screen then its stay on its right screen.
but I don't want this and this not good.
I want when apps check login status its shows a loading screen or anything then after completing its show apps right screen.
how can I do this?
Here is my login status check code
checkLoginStatus() async {
sharedPreferences = await SharedPreferences.getInstance();
if (sharedPreferences.getString("empid") == null) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => Home()),
(Route<dynamic> route) => false);
}
}
Snippet
void main async {
bool isUserLogin = await User.isUserLogin();
if (isUserLogin) { // wait untill user details load
await User.currentUser.loadPastUserDetails();
}
runApp(MyApp(isUserLogin));
}
and
class MyApp extends StatelessWidget {
bool isUserLogin;
MyApp(this.isUserLogin);
#override
Widget build(BuildContext context) {
var defaultRoot = isUserLogin ? HomePage() : LoginScreen();
final material = MaterialApp(
debugShowCheckedModeBanner: false,
home: defaultRoot,
);
return material;
}
}
Hope this helps!
Personnally I like to use a variable _isLoading which I set to true at the beginning of my login method using a setState. Then using this bool you just have to change what is displayed in your Scaffold.
bool _isLoading = false;
checkLoginStatus() async {
setState() => _isLoading = true;
sharedPreferences = await SharedPreferences.getInstance();
if (sharedPreferences.getString("empid") == null) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => Home()),
(Route<dynamic> route) => false);
} else {
setState() => _isLoading = false;
}
}
EDIT: In your case this might be more relevant:
#override
Widget build(BuildContext context) {
switch (currentUser.status) {
case AuthStatus.notSignedIn:
return LoginPage();
case AuthStatus.signedIn:
return HomePage();
default:
return LoadingPage();
}
}
My currentUser is just a class that I've created containing an enum AuthStatus which can have the value notSignedIn or signedIn. If you set the status to null while loading you can display your loading screen.