Flutter provider and widget lifecycle - flutter

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.

Related

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

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'));

Firebase Authorization is not checking if the user is logged in properly

in my flutter app I want to check if a user is logged in before or not and based on this navigate them to either HomePage or SignIn page. The code I am using is not working fine, it is not navigating to the SignIn page after I've done registration and deleted the account in Firebase Console. In short, when I delete a user, who registered well before, in Firebase Console the application is keeping to show the HomePage and all the posts the user made. How can I handle it?
"In short, I want to navigate the user to SignIn page after I delete his account in Firebase Console"
Here is the code in my main.dart file:
_check(){
final FirebaseAuth auth = FirebaseAuth.instance;
final User? user = auth.currentUser;
if(user == null){
HiveDB.deleteUser();
return SignInPage();
}
HiveDB.putUser(id: user.uid);
return HomePage();
}
#override
void initState() {
// TODO: implement initState
super.initState();
_check();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: _check(),
routes: {
SignInPage.id:(context) => SignInPage(),
SignUpPage.id:(context) => SignUpPage(),
HomePage.id:(context) => HomePage(),
},
);
}
The _check() function is not working as it's desired...
While Firebase persists the user info in local storage and automatically restores it when the app/page reloads, this requires that it makes an asynchronous call to the server and thus may not be ready by the time your check runs.
Instead of depending on currentUser, you'll want to listen for changes to the authentication state through the authStateChanges stream. Most apps wrap this in a StreamBuilder and then show the correct screen based on the stream state and last event.
This is something that can be resolved by using async . the problem is you are getting response in future but the action is performed before. Please check the following documentation for detailed overview:
https://dart.dev/codelabs/async-await

How to attend best practice for not using UI code in the Controller with GetX flutter when I need to show a Dialog if my task complete.?

For a simple Email login with OTP code I have a structure as follows.
View
await _signUpCntrl.signUp(email, password);
Controller
_showOtpDialog(email);
_showOtpDialog func
return Get.dialog(
AlertDialog(
So the thing is _showOtpDialog function is inside a controller file. ie. /Controllers/controller_file.dart
I want do something like a blocListener, call the _showOtpDialog from a screen(view) file on signup success. (also relocate the _showOtpDialog to a view file)
Using GetX I have to use one of the builders either obs or getbuilder. Which is I think not a good approach to show a dialog box.
On internet it says Workers are the alternative to BlocListener. However Workers function resides on Controller file and with that the dialog is still being called on the controller file.
As OTP dialog will have its own state and a controller I wanted to put it inside a /view/viewfile.dart
How do I obtain this?
I tried using StateMixin but when I call Get.dialog() it throw an error.
visitChildElements() called during build
Unlike BLoC there's no BlocListener or BlocConsumer in GetX.
Instead GetX has RxWorkers. You can store your response object in a Rx variable:
class SomeController extends GetxController{
final response= Rxn<SomeResponse>();
Future<void> someMethod()async{
response.value = await someApiCall();
}
}
And then right before the return of your widget's build method:
class SomeWidget extends StatelessWidget{
final controller = Get.put(SomeController());
#override
Widget build(BuildContext context){
ever(controller.response, (SomeResponse res){
if(res.success){
return Get.dialog(SuccessDialog()); //Or snackbar, or navigate to another page
}
....
});
return UI();
}
First thing, you will need to enhance the quality of your question by making things more clearly. Add the code block and the number list, highlight those and making emphasize texts are bold. Use the code block instead of quote.
Seconds things, Depends on the state management you are using, we will have different approaches:
Bloc (As you already added to the question tag). By using this state management, you controller ( business logic handler) will act like the view model in the MVVM architecture. In terms of that, You will need to emit a state (e.g: Sent success event). Afterward, the UI will listen to the changes and update it value according to the event you have emitted. See this Bloc example
GetX (As your code and question pointed out): GetX will acts a little bit different. you have multiple ways to implement this:
Using callbacks (passed at the start when calling the send otp function)
Declare a general dialog for your application ( this is the most used when it comes to realization) and calling show Dialog from Bloc
Using Rx. You will define a Reactive Variable for e.g final success = RxBool(true). Then the view will listen and update whenever the success changes.
controller.dart
class MyController extends GetxController {
final success = RxBool(false);
void sendOtp() async {
final result = await repository.sendOTP();
success.update((val) => {true});
}
}
view.dart
class MyUI extends GetView<MyController> {
#override
Widget build(BuildContext context) {
ever(controller.success, (bool success) {
// This will update things whenever success is updated
if (success) {
Get.dialog(AlertDialog());
}
});
return Container();
}
}

pop and push the same route back with different params in Futter (GET X)

I have 2 screens,
Screen one contains a list view with onPressed action on every item
screen two contains the detail of the pressed item as well as a drawer with the same list view as screen one.
What I want to do here is when the user goes to the detail screen and click on an item from the drawer the detail screen should pop and push back with new params.
Code so far,
Route
GetPage(
name: '/market-detail',
page: () => MarketDetail(),
binding: MarketDetailBinding(),
),
Binding
class MarketDetailBinding extends Bindings {
#override
void dependencies() {
Get.lazyPut(() => MarketDetailController());
}
}
Click action in screen one
onTap: () {
Get.toNamed('market-detail',
arguments: {'market': market});
},
Detail Screen Class
class MarketDetail extends GetView<MarketDetailController> {
final Market market = Get.arguments['market'];
}
Click action in detail screen sidebar
onTap: () {
Get.back();
Get.back();
Get.toNamed('market-detail',
arguments: {'market': market});
},
First Get.back() is to close the drawer, then remove the route and push the same route back again,
Expected behaviour,
MarketDetailController should be deleted from memory and placed again,
What actually happening
The controller only got delete and not getting back in memoery on drawer click action until I hot restart the app(By clicking save).
If anybody understands it, please help I am stuck here.
As I can see, you're trying to pop and push the same route with a different parameter in order to update a certain element on that route. Well, if that's the case then just let me show you a much better way.
In your MarketDetailController class you should add those:
class MarketDetailsController extends GetxController {
// A reactive variable that stores the
// instance of the market you're currently
// showing the details of.....
Rx<Market> currentMarket;
// this method will be called once a new instance
// of this controller gets created
// we will use it to initialize the controller
// with the required values
#override
void onInit() {
// some code here....
// .......
// intializing the variable with the default value
currentMarket = Market().obs;
super.onInit();
}
void updateCurrentMarket(Market market) {
// some code here if you need....
// updating the reative variable value
// this will get detected then by the Obx widgets
// and they will rebuild whatever depends on this variable
currentMarket.value = market;
}
}
Now inside your page UI, you can wrap the widget that will display the market details with the Obx widget like this:
Obx(
() {
final Market currentMarket = controller.currentMarket.value;
// now you have the market details you need
// use it and return your widget
return MyAwesomeMarketDetailsWidget();
},
)
Now for your click action, it can just be like this:
onTap: () => controller.updateCurrentMarket(myNewMarketValue)
This should be it. Also, I advise you to change GetView to GetWidget and Get.lazyPut() to Get.put()

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