Flutter - re-run previous page code after execution returns to it - flutter

I'm trying to figure out the best way to implement the proper navigation flow of a flutter app I'm building that involves a 3rd party authentication page (Azure AD B2C). Currently I have a page that serves simply as a "navigate to 3rd party auth login" page which is set as the initialRoute for my app. The first time through, it runs exactly the way I want it to, but I'm not able to figure out how to get that 'navigate to auth' page to re-run when navigated back to (after logout) so that the user ends up back at the 3rd party auth login page.
Basically what I'd like to do is, on logout - have the app navigate back to that page specified as the initialRoute page, and upon that page being navigated back to, have it re-launch the 3rd party auth login page just like it did the first time it executed.
I tried just awaiting the call to Navigator.push() and then calling setState((){}) afterwards, and that does re-display the page, but it just leaves that initial page sitting there, and doesn't end up triggering the execution the way it did the first time. initState() does not fire again, so neither does any of my code that's in there.
I've tried various methods off the Navigator object trying to reload the page or navigate to itself again, or just calling goToLogin() again after the await Navigator.push() call, nothing works.
Here's what I'm currently doing :
User launches the app, the initialRoute is LoginRedirect
class LoginRedirect extends StatefulWidget {
#override
_LoginRedirectState createState() => _LoginRedirectState();
}
class _LoginRedirectState extends State<LoginRedirect> {
#override
void initState() {
Utility.getConfig().then((value) {
config = value;
oauth = AadOAuth(config);
goToLogin(context);
});
super.initState();
}
void goToLogin(BuildContext context) async {
setState(() {
loading = true;
});
try {
await oauth.login(); // this launches the 3rd party auth screen which returns here after user signs in
String accessToken = await oauth.getAccessToken();
navigateToDashboard();
setState(() {
loading = false;
});
} on Exception catch (error) {
setState(() {
loading = false;
});
}
}
void navigateToDashboard() {
await navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => Dashboard()));
// right here is where I'd like to call goToLogin() again after I Navigator.popUntil() back to this
// page, but if I try that I get an error page about how 'The specified child already
// has a parent. You must call removeView() on the child's parent first., java.lang
// .IllegalStateException and something about the bottom overflowed by 1063 pixels
}
}
After getting some config values and calling oauth.login() then I call a navigateToDashboard() method that pushes the Dashboard page on to the navigation stack.
Elsewhere in the code I have a logout button that ends up calling this code:
oauth.logout();
Navigator.popUntil(context, ModalRoute.withName('/LoginRedirect'));
which returns execution to where I called await Navigator.push() previously. But I can't figure out what I need to do there to have that LoginRedirect page execute again. I can't call goToLogin() again or it errors/crashes. I can't call initState() again, calling setState() doesn't do anything. I'm kinda stumped here, I thought this would be easy.

When logging out try: Navigator.pushReplacementNamed(context, "/LoginRedirect"); instead of Navigator.popUntil(context, ModalRoute.withName('/LoginRedirect'));

Related

Riverpod - How to refresh() after an async function completes after the user has already left the screen

I have a Flutter app that has a screen with a ListView of items that is supplied by a provider via an API call. Tapping an item goes to a second "detail" screen with a button (onPressed seen below) that will make an API call to complete an action. On success, the item is removed from the list by running refresh() to make the first API call again to get an updated list. However, if the user navigates away from the detail screen before the action is completed, executing the refresh is no longer possible and will throw the "Looking up a deactivated widget's ancestor is unsafe" exception. This is why I am first checking the mounted property, but is there any way in this scenario to execute the refresh? I am trying to avoid the confusion of the user seeing an outdated list and having to manually refresh. Thanks.
onPressed: () async {
setState(() {
// Disable the submit button
});
someAsyncFunction().then(
(success) {
if (!mounted) {
return;
}
if (success) {
// Success snackBar
ref.refresh(someProvider);
Navigator.pop(context);
} else {
// Failure snackBar
setState(() {
// Re-enable submit button
});
}
},
);
}

Flutter provider and widget lifecycle

I have a simple flow:
a UserModel implementing ChangeProvider which wraps the state of the user (if it is logged in and utilities to log him in/out). In particular logout looks like:
void logout() {
user = null;
notifyListeners();
}
a UserPage widget with (among others):
class UserPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
// ... adding only relevant code
// a text with the user first letter of the email
Text(context.watch<UserModel>().user.email[0])
// a logout button with the following onPressed method
TextButton( \\ ...
onPressed: () {
context.read<UserModel>().logout();
Navigator.pop(context);
}
)
}
}
I was expecting that pressing logout and popping the UserPage widget will not let flutter rebuild it. However it is not the case and the notifyListeners() in logout method makes flutter rebuild the widget and trigger a NullPointerException (since the user is null and the email can't be accessed).
I could deal with it (checking if the user object is != null but I would like to understand why this happens).
Is it correct to assume pop destroys the widget? If not, how should I handle this case?
When a user is logged out I don't want to have in memory this widget nor deal with its existence. I would expect to create a UserPage when a user logs in and destroy it after its logout
When you call logout , watch in this line Text(context.watch<UserModel>().user.email[0]) will cause the widget it's in to rebuild.
You need to call logout in the call back of push the one you called to push this page. pop can also send values to inform about the conditions like success or failure.
So it would be something like this:
Navigator.pushNamed(context, some_route).then(value) {
context.read<UserModel>().logout();
}
value in the call back can be returned from pop like so Navigator.of(context).pop(true);
This will ensure that logout is only called after the widget has been popped.
If you don't like callbacks you can use async await pattern.

Navigating away when a Flutter FutureProvider resolves it's future

I'm basically looking for a way to either start loading data, or navigate to the Login Screen.
The FutureProvider gets it's value from SharedPreferences. The default homescreen is just a logo with a spinner.
If the userID resolves to null, the app should Navigate to the Login Screen, otherwise it should call a method that will start loading data and then on completion navigate to the main page.
Can this be achieved with FutureProvider?
I add it to the page build to ensure the page widget will subscribe to the Provider:
Widget build(BuildContext context) {
userInfo = Provider.of<UserInfo>(context);
print('Building with $userInfo');
return PageWithLoadingIndicator();
....
I added it to didChangeDependencies to react to the change:
#override
void didChangeDependencies() {
print('Deps changed: $userInfo');
super.didChangeDependencies();
// userInfo = Provider.of<UserInfo>(context); // Already building, can't do this.
// print('And now: $userInfo');
if (userInfo == null) return;
if (userInfo.userId != null) {
startUp(); // Run when user is logged in
} else {
tryLogin(); // Navigate to Login
}
}
And since I can't use Provider.of in initState I added a PostFrameCallback
void postFrame(BuildContext context) {
print('PostFrame...');
userInfo = Provider.of<UserInfo>(context);
}
Main is very simple - it just sets up the MultiProvider with a single FutureProvider for now.
class MyApp extends StatelessWidget {
Future<UserInfo> getUserInfo() async {
String token = await UserPrefs.token;
return UserInfo.fromToken(
token,
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App One',
theme: ThemeData(
primarySwatch: Colors.purple,
),
home: MultiProvider(
providers: [FutureProvider<UserInfo>(builder: (_) => getUserInfo())],
child: LoadingScreen(),
),
);
}
}
The problem is that as it is now I can see from the print statements that "didChangeDependencies" gets called twice, but the value of userInfo is always null, even though build() eventually gets an instance of UserInfo as evident by the print statement in the build() method.
I'm guessing I can add some logic into the build method but that screams against my sensibilities as the wrong place to do it... perhaps it isn't as bad as I think?
I've decided that this is conceptually the wrong approach.
In my case I wanted to use a FutureProvider to take the results from an Async function which create a "Config" object using SharedPreferences. The FutureProvider would then allow the rest of the app to access the user's config settings obtained from sharepreferences.
This still feels to me like a valid approach. But there are problems with this from an app flow perspective.
Mainly that the values from the shared preferences includes the logged in user session token and username.
The app starts by showing a Loading screen with a Circular Progress bar. The app then reads the shared preferences and connects online to check that the session is valid. If there is no session, or if it is not valid, the app navigates to the Login "wizzard" which asks username, then on the next page for the password and then on the next page for 2-factor login. After that it navigates to the landing page. If the loading page found a valid session, the login wizzard is skipped.
The thing is that the two things - app state and app flow are tangenially different. The app flow can result in changes being store in the app state, but the app state should not affect the app flow, at least not in this way, conceptually.
In practical terms I don't think calling Navigator.push() from a FutureProvider's build function is valid, even if context is available. I could be wrong about this, but I felt the flowing approach is more Flutteronic.
#override
void initState() {
super.initState();
_loadSharedPrefs().then((_) {
if(this.session.isValid()) _navToLandingPage();
else _navToLoginStepOne();
}
}
I'm open to better suggestions / guidance

How to wait for a method that is already being executed?

I'm developing a Flutter app which has some tabs inside, each of them depend on the database that is loaded on the first run. State is stored in a ScopedModel.
On every tab I have this code:
#override
void initState() {
super.initState();
loadData();
}
void loadData() async {
await MyModel.of(context).scopedLoadData();
_onCall = MyModel.of(context).onCall;
setState(() {
});
}
And this is the code snippet that matters for the ScopedModel:
Future<Null> scopedLoadData() async {
if (_isLoading) return;
_isLoading = true;
(...)
_isLoading = false;
}
If the user waits on the first tab for a few seconds everything is fine, since Database is loaded. However, if the user switches tabs right after app launch, the method scopedLoadData is still being executed so that I get a runtime error ("Unhandled Exception: NoSuchMethodError: The method 'ancestorWidgetOfExactType' was called on null.").
This exception happens because the scopedLoadData has not yet been completed. So I'm looking for a way to wait for a method that is still being executed.
Thanks!
Not sure without seeing your build method but I would start your build method with a guard clause.
if (_oncall == null) return Container(); // or Text("loading") or similar
use should be using a FutureBuilder on each tab to make sure the data is loaded before you try to build the widget... more code would be helpful
I solved the exception by getting rid of every:
setState(() { });
and implementing ScopedModelDescendant on every relevant tab on top of using notifyListeners() at the end of the database loading method.
This pulls the responsibility from the tabs for updating views and gives it to the scopedLoadData().

How to go back to the previous screen and reload all function of the first screen?

I have a problem. How to go back to the previous screen and reload all function of the first screen?
On page 1, I have several functions to load all data from json and calculations.
And when user goes to page 2 and does an insert to the json and when the suer goes back to page 1, reload all the functions on page 1.
I have function like below:
Future<void> CountBalanced() {
ListDetailPaymentTransactions(global.strtransidtrans);
global.doubbalanced = global.inttotalpayment - global.doubtranstotaltrans;
print(global.inttotalpayment - global.doubtranstotaltrans);
}
Currently I found this code:
Navigator.push(context, MaterialPageRoute(builder: (context) => CashPayment())).whenComplete(CountBalanced);
But the issue is when the user goes back to page 1 the functions are running all the time and wont stop.
I noticed it when I tried to print the result.
Is there any way to stop it? And is there any better way to reload all the function on page 1?
For stopping your function when navigation try disposing them
#override
void dispose() {
super.dispose();
}
To reloading the methods
#override
void didUpdateWidget(ClassName oldWidget) {
super.didUpdateWidget(oldWidget);
if (comparision) {
//your initState
}
}
For more information
https://docs.flutter.io/flutter/widgets/State/didUpdateWidget.html