How to popUntil to a named route in Flutter? - flutter

I am trying to implement a signin_screen.dart which after taking the user's email and password, checks for a stream of authStateChanges. If the login is successful it takes automatically takes the user to the account screen (which a named route: "/my-account"). I'm trying to display a CircularProgressIndiciator in a showDialog for the time between the user submitting his details by clicking on the sign in button till the time the login process is completed.
My Code:
signin_screen.dart
return SafeArea(
child: Scaffold(
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return const AccountScreen(); //named-route of "/my-account"
} else {
return CustomLoginWidget();
}
The following function is called when the sign in button (implemented in the CustomLoginWidget) is clicked :
void signInUser() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return const Center(
child: CircularProgressIndicator(),
);
});
context.read<FirebaseAuthMethods>().loginWithEmail(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
context: context);
navigatorKey.currentState!.popUntil((route) => route.isFirst);
//Navigator.popUntil(context, ModalRoute.withName("/my-account"));
}
Note:
Navigator.popUntil(context, ModalRoute.withName("/my-account")); does not work.
navigatorKey: navigatorKey,
has been added as a property in MaterialApp(), where navigatorKey has been declared as final navigatorKey = GlobalKey<NavigatorState>();
Issue Faced:
If I do navigatorKey.currentState!.popUntil((route) => route.isFirst it takes the app to the splash screen (as named by the "/" route), but I want the app to load the account_screen under the route name: "/my-account".
Is there a way to implement this? I have tried route.isCurrent and route.isActive, but to no avail.
Please help.

How about try to declare a route name for your MyAccount class
static const routeName = "/my-account"
and then try this
navigatorKey.currentState!.popUntil((route) => route.settings.name == MyAccount.routeName);

Related

Navigate to another page in StreamBiulder on Flutter

I'm Trying to know if the user is logged in or not using firebase google Auth and send him to different pages depending the case.
If the user is Logged in, Streambuilder returns profile() and if not, signUp() is returned. So far, so good. But what I need is to Navigate to another page using Navigator instead of returning widgets.
I need to do this:
Navigator.push( context, MaterialPageRoute(builder: (context) => profile()),
Instead of:
return profile();
The code I'm working on is:
body: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasData) {
return profile();
} else if (snapshot.hasError) {
return Center(child: Text("Something went wrong"));
} else {
return signUp();
}
},
),
Any idea on how to do this? Should I use other approach instead of a Streambuilder? Thanks in advance!
the FirebaseAuth.instance.authStateChanges() returns a Stream, so you can listen to it inside your app and make acts based on it, instead of using it in a StreamBuilder, you can listen to it like this:
FirebaseAuth.instance.authStateChanges().listen((user) {
if(user != null) {
Navigator.push( context, MaterialPageRoute(builder: (context) => profile()),
} else {
Navigator.push( context, MaterialPageRoute(builder: (context) => Login()),
}
});
you need just to find a place where you're going to call it, once it listens to the authStateChanges(), by authenticating a new user or logging him out, this stream will trigger the change and execute the code inside of it.

How to call api once in futurebuilder

My application have different routes and I would like to know how to call my api with cubit just once when the user come for the first time on the screen and also not to re-call the api every time he returns to the screen already initialized.
my structure use bloC
and this is my profile page initialization class
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final user = context.read<AuthCubit>().state;
final bloc = context.read<ProfileCubit>();
return Scaffold(
body: FutureBuilder(
future: bloc.updateProfilePicture(user!.id),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return BlocBuilder<ProfileCubit, ProfilePicture?>(
buildWhen: (prev, curr) => prev != curr,
builder: (context, picture) {
return picture != null
? Profil(profilePicture: picture, updateIndex: updateIndex)
: Profil(updateIndex: updateIndex);
},
);
}
return Center(
child: CircularProgressIndicator(
color: Colors.orange,
),
);
},
),
);
}
There are many ways to solve this problem
1- easy (but not clean code) is to use boolean global varibal
like isApiReqursted with default value (false) and when call the api set it to true
2- you can cache the response in the repoistory or bloc and make the api method frst check if there are data if there isit does not need to make http request

Declarative auth routing with Firebase

Rather than pushing the user around with Navigator.push when they sign in or out, I've been using a stream to listen for sign in and sign out events.
StreamProvider<FirebaseUser>.value(
value: FirebaseAuth.instance.onAuthStateChanged,
)
It works great for the home route as it handles logging in users immediately if they're still authed.
Consumer<FirebaseUser>(
builder: (_, user, __) {
final isLoggedIn = user != null;
return MaterialApp(
home: isLoggedIn ? HomePage() : AuthPage(),
// ...
);
},
);
However, that's just for the home route. For example, if the user then navigates to a settings page where they click a button to sign out, there's no programmatic logging out and kicking to the auth screen again. I either have to say Navigator.of(context).pushNamedAndRemoveUntil('/auth', (_) => false) or get an error about user being null.
This makes sense. I'm just looking for possibly another way that when they do get logged out I don't have to do any stack management myself.
I got close by adding the builder property to the MaterialApp
builder: (_, widget) {
return isLoggedIn ? widget : AuthPage();
},
This successfully moved me to the auth page after I was unauthenticated but as it turns out, widget is actually the Navigator. And that means when I went back to AuthPage I couldn't call anything that relied on a parent Navigator.
What about this,you wrap all your screens that depend on this stream with this widget which hides from you the logic of listening to the stream and updating accordingly(you should provide the stream as you did in your question):
class AuthDependentWidget extends StatelessWidget {
final Widget childWidget;
const AuthDependentWidget({Key key, #required this.childWidget})
: super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {//you handle other cases...
if (snapshot.currentUser() != null) return childWidget();
} else {
return AuthScreen();
}
},
);
}
}
And then you can use it when pushing from other pages as follows:
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (ctx) => AuthDependentWidget(
childWidget: SettingsScreen(),//or any other screen that should listen to the stream
)));
I found a way to accomplish this (LoVe's great answer is still completely valid) in case anyone else steps on this issue:
You'll need to take advantage of nested navigators. The Root will be the inner navigator and the outer navigator is created by MaterialApp:
return MaterialApp(
home: isLoggedIn ? Root() : AuthPage(),
routes: {
Root.routeName: (_) => Root(),
AuthPage.routeName: (_) => AuthPage(),
},
);
Your Root will hold the navigation for an authed user
class Root extends StatefulWidget {
static const String routeName = '/root';
#override
_RootState createState() => _RootState();
}
class _RootState extends State<Root> {
final _appNavigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final canPop = _appNavigatorKey.currentState.canPop();
if (canPop) {
await _appNavigatorKey.currentState.maybePop();
}
return !canPop;
},
child: Navigator(
initialRoute: HomePage.routeName,
onGenerateRoute: (RouteSettings routeSettings) {
return MaterialPageRoute(builder: (_) {
switch (routeSettings.name) {
case HomePage.routeName:
return HomePage();
case AboutPage.routeName:
return AboutPage();
case TermsOfUsePage.routeName:
return TermsOfUsePage();
case SettingsPage.routeName:
return SettingsPage();
case EditorPage.routeName:
return EditorPage();
default:
throw 'Unknown route ${routeSettings.name}';
}
});
},
),
);
}
}
Now you can unauthenticate (FirebaseAuth.instance.signout()) inside of the settings page (or any other page) and immediately get kicked out to the auth page without calling a Navigator method.

How to call provider on condition?

On app homepage I set up Model2 which make API call for data. User can then navigate to other page (Navigator.push). But I want make API call from Model2 when user press back (_onBackPress()) so can refresh data on homepage.
Issue is Model2 is not initialise for all user. But if I call final model2 = Provider.of<Model2>(context, listen: false); for user where Model2 is not initialise, this will give error.
How I can call Provider only on condition? For example: if(user == paid)
StatefulWidget in homepage:
#override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<Model1, Model2>(
initialBuilder: (_) => Model2(),
builder: (_, model1, model2) => model2
..string = model1.string,
),
child: Consumer<Model2>(
builder: (context, model2, _) =>
...
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(context: context)),
In Page2:
Future<void> _onBackPress(context) async {
// if(user == paid)
final model2 = Provider.of<Model2>(context, listen: false);
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return
// if(user == paid)
Provider.value(value: model2, child:
AlertDialog(
title: Text('Back'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('Go back'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('OK'),
onPressed: () async {
// if(user == paid)
await model2.getData();
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
Alternative method (maybe more easy): How to call provider on previous page (homepage) on Navigator.of(context).pop();?
TLDR: What is best solution for call API so can refresh data when user go back to previous page (but only for some user)?
You can wrap your second page interface builder in a WillPopScope widget, and then, pass whatever method you want to call to the onWillPop callback of the WillPopScope widget. This way, you can make your API call when user presses the back button. Find more about the WillPopScope widget on this WillPopScope Flutter dev documentation article.
tldr; Establish and check your single point of truth before the call to the Provider
that may result in a null value or evaluate as a nullable reference.
Perhaps you can change the architecture a bit to establish a single (nullable or bool) reference indicating whether the user has paid. Then use Darts nullability checks (or just a bool) to implement the behavior you want. This differs from your current proposal in that there would be no need to call on the Provider to instantiate the model. Just add a single point of truth to your User object that is initialized to null or false, and then change that logic only when the User has actually paid.
Toggling widgets/behavior in this way could be a solution.
Alternatives considered:
Packaging critical data points into a separate library so that the values can be imported where needed.
Other state management methods for key/value use.
If you want to simply hide/show parts of a page consider using the OffStage class or the Visibility class
Ref
Dart null-checking samples

Flutter how to redirect to login if not authorized

I'm trying to redirect to login in case the token has been expired in Flutter
Trying to get the posts:
body: new Container(
padding: new EdgeInsets.only(bottom: 8.0),
color: Color(0xff2c3e4e),
child: FutureBuilder<List<Post>>(
future: api.getposts(),
builder: (context, snapshot) {
// doing stuff with response
}
)
)
getposts and catching the error:
Future<List<Post>> getposts() async {
url = 'url';
var response = await http.get(url,
headers: {HttpHeaders.authorizationHeader: 'bearer ' + token},
);
//part I can't understand how to get to work, how do I push? This gives error about context not being defined.
if (response.body.toString().contains('Token is Expired')) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
LoginScreen()),
);
}
}
So the question is, how do I use the Navigator correctly in such cases and I can redirect back to loginScreen in case the token has been expired and needs to be renewed? Like mentioned in the comment in the code example, the context gives me "undefined".
Is this even possible the way I am doing it or am I simply handling the whole check completely wrong?
Code should have single resonpsibility. Your getPost method are doing 2 things at the same time. You should break this function such that it either successfully get the the post, or throw exception, and its caller will handle the exception. Its caller btw must be within build method, because only build method has BuildContext context, something like this:
if (response.body.toString().contains('Token is Expired')) {
throw new Exception("Token is Expired") // you may want to implement different exception class
}
body: new Container(
padding: new EdgeInsets.only(bottom: 8.0),
color: Color(0xff2c3e4e),
child: FutureBuilder<List<Post>>(
future: api.getposts(),
builder: (context, snapshot) {
if (snapshot.hasError) {
// you might want to handle different exception, such as token expires, no network, server down, etc.
Navigator.push(
context,
MaterialPageRoute(builder: (context) => LoginScreen()),
);
return Container(); // return empty widget
}
if (snapshot.hasData) {
// do somthing with data
return Text('Some data here');
}
// has no data
return Text("Loading...");
}),
),
UPDATE
Thanks to #TruongSinh I got it figured out.
Followed his example and figured out the build navigator method which works:
if (snapshot.hasError) {
#override
void run() {
scheduleMicrotask(() {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => LoginScreen()),
);
});
}
run();
}
Update: added a package containing several guards like this
I did it with a StreamBuilder to react on change and be able to display a LoadingScreen when we don't know yet if the user is connected.
StreamBuilder authGuard = StreamBuilder(
stream: Auth.authState$,
builder: (context, snapshot) {
switch (snapshot.data) {
case AuthState.PENDING:
return LoadingScreen();
case AuthState.UNAUTHENTICATED:
return SignInScreen();
case AuthState.AUTHENTICATED:
return HomeScreen();
default:
return LoadingScreen();
}
},
);
So it will change screen depending on the AuthState:
return MaterialApp(
// ...
home: authGuard,
);
And my auth class
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:rxdart/rxdart.dart';
enum AuthState { PENDING, AUTHENTICATED, UNAUTHENTICATED }
class Auth {
static final FirebaseAuth _auth = FirebaseAuth.instance;
static final GoogleSignIn _googleSignIn = GoogleSignIn();
static Stream<AuthState> authState$ = FirebaseAuth.instance.onAuthStateChanged
.map((state) =>
state != null ? AuthState.AUTHENTICATED : AuthState.UNAUTHENTICATED)
.startWith(AuthState.PENDING);
static Future<FirebaseUser> signInWithGoogle() async {
// ...
}
}