Go back to login when logged out from the drawer, no matter what - flutter

I need to redirect user to login page when he clicks on logout button from drawer (wherever he is). The problem is that when I click on the logout button, the screen remains the same.
According to this post: Flutter provider state management, logout concept
I have:
void main() async {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Profile>(
create: (final BuildContext context) {
return Profile();
},
)
],
child: MyApp(),
),
);
}
MyApp:
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
initPlatformState();
}
/// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
if (!mounted) return;
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
initialRoute: '/',
navigatorKey: navigatorKey,
// ...
home: Consumer<Profile>(
builder: (context, profile, child){
return profile.isAuthenticated ? SplashScreen() : AuthScreen();
}
)
);
}
}
The part of the drawer where there is the logout button:
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context)!.logout),
onTap: () async {
SharedPreferences preferences =
await SharedPreferences.getInstance();
await preferences.clear();
final Profile profile =
Provider.of<Profile>(context, listen: false);
profile.isAuthenticated = false;
}),
As I said, when I click on the logout button from the drawer, the user is correctly logged out, but the screen remains the same.
UPDATE
This is the profile class:
class Profile with ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated {
return this._isAuthenticated;
}
set isAuthenticated(bool newVal) {
this._isAuthenticated = newVal;
this.notifyListeners();
}
}

I think you are using provider class incorrectly.
use your profile class like this.
class Profile with ChangeNotifier {
bool _isAuthenticated = true;
bool get getIsAuthenticated => _isAuthenticated;
set setIsAuthenticated(bool isAuthenticated) {
_isAuthenticated = isAuthenticated;
notifyListeners();//you must call this method to inform lisners
}
}
in set method call notifyListners();
in your listTile
replace profile.isAuthenticated = false to profile.isAuthenticated = false;
Always use getters and setters for best practice.
I hope this is what you were looking for.

Add Navigator.of(context).pushReplacementNamed("/routeName") in LogOut onTap() Section.
For more information : https://api.flutter.dev/flutter/widgets/Navigator/pushReplacementNamed.html
Make sure to have logout route set in MyApp file, and i'd edit logout button file as such:
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context)!.logout),
onTap: () async {
SharedPreferences preferences =
await SharedPreferences.getInstance();
await preferences.clear();
final Profile profile =
Provider.of<Profile>(context, listen: false);
profile.isAuthenticated = false;
// add login file route here using Navigator.pushReplacementNamed() ;
}),

Navigator push named -> logout route?

Related

How can I use condition and go to different page on flutter using Getx without click

I am trying to use firebase data to route different pages using Getx. First I have a splash screen and want to automatically go to different pages according to conditions. If the user has already login, it will redirect the Home page, if not the route to the login page. But I can't use initState() on the Stateless widget as I using Getx, I don't want a Stateful widget.
class SplashPage extends StatelessWidget {
RxBool isloading = true.obs;
#override
Widget build(BuildContext context) {
String Uid = "";
return isloading.value
? SpinKitThreeInOut(
color: Colors.red,
)
: Obx(() {
return Get.find<AuthController>().user != null
? homeMethod()
: login();
});
}
Widget homeMethod() {
return Home(AuthController.instance.user.toString());
isloading.value = false;
}
}
But I ain't able to override isloading.value = false;
My Getx Auth Controller:
class AuthController extends GetxController {
static AuthController instance = Get.find();
FirebaseAuth auth = FirebaseAuth.instance;
Rxn<User> _firebaseUser = Rxn<User>();
String? get user => _firebaseUser.value?.uid;
#override
void onReady() {
// TODO: implement onReady
super.onReady();
_firebaseUser.value = auth.currentUser;
_firebaseUser.bindStream(auth.userChanges());
ever(_firebaseUser, _initialScreen);
}
/* #override
void onInit() {
_firebaseUser.bindStream(_auth.authStateChanges());
}*/
_initialScreen(User? user) {
if (user == null) {
Get.offAll(login());
} else {
String userId = user.uid;
Get.offAll(Home(userId));
}
}
Future<User?> LogInAccounts(String Email, String Password) async {
FirebaseAuth auth = FirebaseAuth.instance;
try {
User? user = (await auth.signInWithEmailAndPassword(
email: Email, password: Password))
.user;
if (user != null) {
Fluttertoast.showToast(msg: "Account Create Sucessfully");
return user;
} else {
Fluttertoast.showToast(msg: "Account Create Failed!");
return user;
}
} catch (e) {
return null;
}
}
}
Updated Answer
You can use bindStream and do it that way, but instead of trying to turn your User object into a stream this can be done with a simple RxBool. Firebase already provides a function to listen to auth state changes.
class AuthController extends GetxController {
RxBool loggedIn = false.obs;
#override
void onInit() {
super.onInit();
_subscribe();
}
void _subscribe() {
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user == null) {
loggedIn(false);
log('User is currently signed out');
} else {
loggedIn(true);
log('User is signed in');
}
});
}
}
Then you can add another couple methods to your GetX class.
void initNaviationListener() {
/// inital startup naviation
_navigateBasedOnLogin();
/// future navigation based on auth state changes
ever(loggedIn, (value) {
_navigateBasedOnLogin();
});
}
void _navigateBasedOnLogin() {
if (loggedIn.value == false) {
Get.offAndToNamed(LoginPage.id);
} else {
Get.offAndToNamed(HomePage.id);
}
}
Then you can call initNaviationListener in the onReady of GetMaterialApp
GetMaterialApp(
/// onReady is called after GetMaterialApp is fully initialized
onReady: () => Get.find<AuthController>().initNaviationListener(),
theme: ThemeData.dark(),
initialRoute: LoginPage.id,
getPages: [
GetPage(
name: SplashPage.id,
page: () => SplashPage(),
),
GetPage(
name: HomePage.id,
page: () => HomePage(),
),
GetPage(
name: LoginPage.id,
page: () => LoginPage(),
),
],
)
That will navigate on app start to the corresponding screen and also respond to any future changes in auth status.
Original Answer
You don't have to navigate from the SplashPage you can do it from the controller.
Let's say your GetMaterialApp looks like this. This takes you to SplashPage first.
GetMaterialApp(
initialRoute: SplashPage.id,
getPages: [
GetPage(
name: SplashPage.id,
page: () => SplashPage(),
),
GetPage(
name: HomePage.id,
page: () => HomePage(),
),
GetPage(
name: LoginPage.id,
page: () => LoginPage(),
),
],
)
Then check logged in status and navigate to the corresponding screen from your AuthController.
class AuthController extends GetxController {
#override
void onInit() {
super.onInit();
_navigateBasedOnLogin();
}
Future<void> _navigateBasedOnLogin() async {
final loggedIn = await _isLoggedIn();
if (loggedIn) {
Get.offAndToNamed(HomePage.id); // offAndToNamed will remove the SplashScreen from the navigation stack
} else {
Get.offAndToNamed(LoginPage.id);
}
}
Future<bool> _isLoggedIn() async {
/// run your code to check logged in status and return true or false
}
}
Then just init the AuthController in your main.
void main() async {
Get.put(AuthController());
runApp(MyApp());
}
With this setup, your SplashScreen can be a generic loading screen with zero logic.
You can handle initialRoute of GetMaterialApp using isLogin flag
class _MyAppState extends State<MyApp> {
bool isLogin = false;
#override
void initState() {
isLogin = isAlreadyLogin();// Your function to check is user logged in.
super.initState();
}
#override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Rider App',
translationsKeys: AppTranslation.translationsKeys,
locale: Get.find<CacheManager>().getLocale(),
getPages: AppPages.pages,
initialRoute: isLogin ? Routes.homeScreen : Routes.loginScreen,
initialBinding: InitialBinding(),
);
}
class Routes {
static const homeScreen = '/home-screen';
static const loginScreen = '/login-screen';
}
class AuthController extends GetxController {
late Rx<User?> firebaseUser;
#override
void onReady() async {
super.onReady();
firebaseUser = Rx<User?>(FirebaseAuth.instance.currentUser);
firebaseUser.bindStream(firebaseAuth.instance.userChanges());
ever(firebaseUser, _setInitialScreen);
}
_setInitialScreen(user) async{
if (user != null) {
Get.offAllNamed(Routes.home);
} else {
Get.offAllNamed(Routes.login);
}
}
}

How to use shared preferences to have a one-time login in flutter?

I searched google/stackoverflow alot and tried but nothing worked. This is the flow i want:
User launches the app. Splash screen appears.
Login screen appears after splash screen. User logs in.
User kills(closes) the app.
When user relaunches the app, it should show the splash screen followed by the homepage, as user has already logged in once before.
User will only see login page if he/she logs out.
So far 1 and 2 works. But when user kills/closes the app and relaunches it again, instead of being directed to the home page, they are directed to the login page again.
The code for the splash screen:
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
startTimer();}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: 150,
width: 150,
child: new SvgPicture.asset(
'assets/logo.png'
),
),
),
);
}
void startTimer() {
Timer(Duration(seconds: 3), () {
navigateUser(); //It will redirect after 3 seconds
});
}
void navigateUser() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
var status = prefs.getBool('isLoggedIn');
print(status);
if (status == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => HomePage());
} else {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => LoginScreen()));
}
}}
The code for the log out button:
void logoutUser() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs?.clear();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (BuildContext context) => SplashScreen()),
ModalRoute.withName("/login"),
);
}
Sorry for the lengthy post, and really appreciate the help if someone could point out where i've gone wrong. Or if there's any other way to achieve a one-time login in flutter. Thanks!
I know my answer is late now. But, If you are using FirebaseAuth, this will automatically cache your login, logout log. So you will not need to store it to pref. Just nee to make additional step when you lauch screen to check if user's last status was login or log out by the following. And this information can be used to rediret to the desired screen.
Code:
Future<bool> checkIfAlreadySignedIn () async {
late bool _isAlreadySignedIn;
await FirebaseAuth.instance
.authStateChanges()
.listen((event) async {
if (event == null) {
print('Currentyl signed out');
_isAlreadySignedIn = false;
} else {
_isAlreadySignedIn = true;
}
});
return _isAlreadySignedIn;
}
Where do you set 'isLoggedIn' pref to true?

I have a question about navigating to the next page conditionally in initstate

I want to implement Auto Login with Shared preferences.
What I want to implement is that as soon as 'LoginPage' starts, it goes to the next page without rendering LoginPage according to the Flag value stored in Shared preferences.
However, there is a problem in not becoming Navigate even though implementing these functions and calling them from initstate. What is the problem?
//Login Page
void autoLogIn() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String userId = prefs.getString('username');
print("ddddddddddddddd");
SocketProvider provider = Provider.of<SocketProvider>(context);
Future.delayed(Duration(milliseconds: 100)).then((_) {**//I tried giving Delay but it still didn't work.**
Navigator.of(context).pushNamedAndRemoveUntil("/MainPage", (route) => false);
});
}
#override
void initState() {
// TODO: implement initState
loginBloc = BlocProvider.of<LoginBloc>(context);
if(!kReleaseMode){
_idController.text = "TESTTEST";
_passwordController.text = "1234123";
}
initBadgeList();
autoLogIn();**//This is the function in question.**
super.initState();
print("1111111111111111");
}
I don't think you should show LoginPage widget if user is already logged in and then navigate to main page.
I suggest you to use FutureBuilder and show either splash screen or loader while performing await SharedPreferences.getInstance(). In this case your App widget should look like this:
class App extends MaterialApp {
App()
: super(
title: 'MyApp',
...
home: FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (snapshot.data != null) {
final SharedPreferences prefs = snapshot.data;
final userId = prefs.getString('username');
...
return userId == null ?? LoginPage() : MainPage();
} else {
return SplashScreenOrLoader();
}
}));
}
But if you still want to show LoginPage first, just replace SplashScreenOrLoader() with LoginPage() in code above.

Show the introduction page only when the user logs in for the first time for a Flutter app

I have used the below code to show the introduction page only when the user logs in for the first time and second time when the user logs in the user should be taken directly to the homepage.
But this does not work after my first try. Now every time a new user logs in it goes directly to the homepage but not to the introduction page. Please let me know if I am doing anything wrong.
Here is my Code:
class OneTimeScreen extends StatefulWidget {
#override
OneTimeScreenState createState() => new OneTimeScreenState();
}
class OneTimeScreenState extends State<OneTimeScreen>
with AfterLayoutMixin<OneTimeScreen> {
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _seen = (prefs.getBool('seen') ?? false);
if (_seen) {
Navigator.of(context)
.pushReplacement(MaterialPageRoute(builder: (context) => HomePage()));
} else {
await prefs.setBool('seen', true);
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => IntroVideoPage()));
}
}
#override
void afterFirstLayout(BuildContext context) => checkFirstSeen();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
This is because the scope of the boolean which you are storing in SharedPreferences as seen is app-wide. It does not differentiate if userA or userB logs in. It is a single boolean for both.
To make that boolean user specific, we can add a unique userID as a prefix to the key seen like..
bool _seen = (prefs.getBool(userID + 'seen') ?? false);
prefs.setBool(userID + 'seen', true);
This will ensure storing different boolean for each user.

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.