Flutter user roles that lead to different screens (firebase) - flutter

I am new to developing in flutter, and I am trying to make an app that will redirect the user to a different screen depending on what role they are as a user. I technically need to implement this in two places, in my the build of my main, and when the log in button is pressed.
What I have been trying to do right now is in the main, check if the user is logged in, then get his user uid, then use that uid to query the database for his role. It seems like a very crude solution as the application feels very choppy + its crashing and it takes it a while at boot to redirect to the right page and I'm unsure if I should be using real time database instead of the normal one for this. Any pointers on how to optimize this would be greatly appreciated.
class MyApp extends StatelessWidget {
late String userRole;
late String path;
#override
Widget build(BuildContext context) {
String userUid = FirebaseAuth.instance.currentUser!.uid;
userRole = "uprav";
path = "";
if(FirebaseAuth.instance.currentUser!.uid.isNotEmpty)
{
Database.setUserUid(userUid);
Database.getRole().then((value) {
userRole = value;
});
switch(userRole) {
case "uprav":
path = "/repair-flow";
break;
case "majs":
path = "/majstor-flow";
break;
case "pred":
path = "/repair-flow";
break;
}
}
print(userRole + " this is .then");
return ScreenUtilInit(
designSize: Size(375, 812),
builder: () => MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Majstor',
theme: ThemeData(
primaryColor: Constants.primaryColor,
scaffoldBackgroundColor: Color.fromRGBO(255, 255, 255, 1),
visualDensity: VisualDensity.adaptivePlatformDensity,
textTheme: GoogleFonts.openSansTextTheme(),
),
initialRoute: FirebaseAuth.instance.currentUser == null ? "/" : path,
onGenerateRoute: _onGenerateRoute,
),
);
}
}
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case "/":
return MaterialPageRoute(builder: (BuildContext context) {
return Home();
});
case "/repair-flow":
return MaterialPageRoute(builder: (BuildContext context) {
return RequestServiceFlow();
});
case "/majstor-flow":
return MaterialPageRoute(builder: (BuildContext context){
return MajstorServiceFlow();
});
default:
return MaterialPageRoute(builder: (BuildContext context) {
return Home();
});
}
}

In flutter, we use need to manage the states of screens.
In your case,
State 1: Loading data - Show loading widget
State 2: Loaded data, you need to update layout - Show the screen based on the role.
There are many ways to do so:
Future builder (https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html)
BLOC(this is a bit hard for beginner but worth in the future development).

Related

How Can I navigate to different screens on startup based on which type of user<Admin, Customer> is logged in by making the use of Streams

In my flutter app there are two kinds of users, Admin and Customer. I wish to implement a functionality which will navigate The Customer to CustomerHomePage() and Admin to AdminHomePage().
I have wrapped my home property of MaterialApp with StreamBuilder which should to listen to any added values to the currentUserStream and alter the UI accordingly :
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
// home: const MyHomePage(title: 'Flutter Demo Home Page'),
home: Scaffold(
body: StreamBuilder(
initialData: null,
stream: FirestoreServices.currentUserStream,
builder: (context, snapshot) {
Widget widget = LogInPage();
if (snapshot.data != null) {
// Go to AdminHomePage if the logged in User is a Admin
print("Logged in Usertype : ${snapshot.data!.userType.toString()}");
if (snapshot.data!.userType == UserType.admin) {
widget = AdminHomePage(caUser: snapshot.data!);
}
// Go to CustomerHomePage if the logged in User is a Customer
else if (snapshot.data?.userType == UserType.customer) {
widget = CustomerHomePage(
caUser: snapshot.data!,
);
}
} else {
widget = LogInPage();
}
return widget;
}),
));
}
}+
the Stream so used in this streamBuilder is is a static property of FirestoreServices Class which is made the following way :
static Stream<CAUser> get currentUserStream async*{
FirebaseAuth.instance.authStateChanges().map(
(event) async* {
yield await FirestoreServices().uidToCAUser(event!.uid);
});
}
According to me the problem that's occuring is the values are either not getting added to the stream or they aren't getting read by the StreamBuilder. The effect of this is that the screen isn't navigationg to any of the HomePages
I tried the code which I just posted above, and I expect there's something wrong with the getter function.
type here

Flutter uni_links navigate to specific page with requestparam

I have using uni_links: ^0.5.1 & get: ^4.3.4 for deep linking.
main.dart router setup :
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Navigator 2.0 Deep Link',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerDelegate: routerDelegate,
routeInformationParser: const MyRouteInformationParser(),
);
below is route details :
MaterialPage _createPage(RouteSettings routeSettings) {
Widget child;
switch (routeSettings.name) {
case '/':
child = const HomePage();
break;
case '/college':
child = College(arguments: routeSettings.arguments as Map<String,dynamic>);
break;
case '/hostel':
child = Hostel();
break;
case '/stadium':
child = Stadium();
break;
default:
child = errorWidget();
}
return MaterialPage(
child: child,
key: Key(routeSettings.toString()) as LocalKey,
name: routeSettings.name,
arguments: routeSettings.arguments,
);
}
After navigating I can able to receive data from parameter and when user refresh the screen i can able to restore param data using below codes.
#override
RouteInformation restoreRouteInformation(List<RouteSettings> configuration) {
final location = configuration.last.name;
final arguments = _restoreArguments(configuration.last);
return RouteInformation(location: '$location$arguments');
}
String _restoreArguments(RouteSettings routeSettings) {
if(routeSettings.name == '/college') {
return '?id=${(routeSettings.arguments as Map)['id'].toString()}';
} else {
return '';
}
}
Everything fine and works.
my question is I have feeling like data leaking because of passing data from param. so is there any other way to hold the data when user refresh the screen without getting from Param or local storage.
http://localhost:64042/#/college?id=7
thanks...

Flutter Provider rebuilt widget before parent's Consumer

I have got a problem with the provider package.
I want to be able to clean an attribute (_user = null) of a provider ChangeNotifier class (it is a logout feature).
The problem is when I am doing that from a Widget that use info from this Provider.
My main app is like :
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => AuthProvider(),
builder: (context, _) => App(),
),
);
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (_, auth, __) {
Widget displayedWidget;
switch (auth.loginState) {
case ApplicationLoginState.initializing:
displayedWidget = LoadingAppScreen();
break;
case ApplicationLoginState.loggedIn:
displayedWidget = HomeScreen();
break;
case ApplicationLoginState.loggedOut:
default:
displayedWidget = AuthenticationScreen(
signInWithEmailAndPassword: auth.signInWithEmailAndPassword,
registerAccount: auth.registerAccount,
);
}
return MaterialApp(
title: 'My App',
home: displayedWidget,
routes: {
ProfileScreen.routeName: (_) => ProfileScreen(),
},
);
});
}
}
My Provider class (simplified) :
class AuthProvider extends ChangeNotifier {
ApplicationLoginState _loginState;
ApplicationLoginState get loginState => _loginState;
bool get loggedIn => _loginState == ApplicationLoginState.loggedIn;
User _user;
User get user => _user;
void signOut() async {
// Cleaning the user which lead to the error later
_user = null;
_loginState = ApplicationLoginState.loggedOut;
notifyListeners();
}
}
My Profile screen which is accessible via named Route
class ProfileScreen extends StatelessWidget {
static const routeName = '/profile';
#override
Widget build(BuildContext context) {
final User user = Provider.of<AuthProvider>(context).user;
return Scaffold(
// drawer: AppDrawer(),
appBar: AppBar(
title: Text('Profile'),
),
body: Column(
children: [
Text(user.displayName),
FlatButton(
child: Text('logout'),
onPressed: () {
// Navigator.pushAndRemoveUntil(
// context,
// MaterialPageRoute(builder: (BuildContext context) => App()),
// ModalRoute.withName('/'),
// );
Provider.of<AuthProvider>(context, listen: false).signOut();
},
)
],
),
);
}
}
When I click the logout button from the profile screen, I don't understand why i get the error :
As I am using a Consumer<AuthProvider> at the top level of my app (this one includes my route (ProfileScreen), I thought it would redirect to the AuthenticationScreen due to the displayedWidget computed from the switch.
But it seems to rebuild the ProfileScreen first leading to the error. the change of displayedWidget do not seems to have any effect.
I'm pretty new to Provider. I don't understand what I am missing in the Provider pattern here ? Is my App / Consumer wrongly used ?
I hope you can help me understand what I've done wrong here ! Thank you.
Note : the commented Navigator.pushAndRemoveUntil redirect correctly to the login screen but I can see the error screen within a few milliseconds.
Your user is null, and you tried to get the name of him. You need to check it before using it. It will look like this:
user == null ?
Text("User Not Found!"),
Text(user.displayName),
From the provider API reference of Provider.of :
Obtains the nearest Provider up its widget tree and returns its
value.
If listen is true, later value changes will trigger a new State.build
to widgets, and State.didChangeDependencies for StatefulWidget.
So I think the line final User user = Provider.of<AuthProvider>(context).user; in your profile screen calls a rebuild when the _user variable is modified, and then the _user can be null in your ProfileScreen.
Have you tried to Navigator.pop the profile screen before clearing the _user variable?

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.

Opening keyboard causes stateful widgets to be re-initialized

I am using Flutter 1.2.1 in the Stable branch. To illustrate my problem imagine I have pages A and B. A navigates to B using Navigator.push and B navigates back to A using Navigator.pop. Both are stateful widgets.
When I navigate from A to B and then pop back to A everything is fine and A keeps its state. However, if I navigate from A to B, tap a textfield in B opening the keyboard, then close the keyboard and pop back to A, A's entire state is refreshed and the initState() method for A is called again. I verified this by using print statements.
This only happens when I open the keyboard before popping back to A. If I navigate to B, then immediately navigate back to A without interacting with anything then A keeps its state and is not re-initialized.
From my understanding the build method is called all the time but initState() should not get called like this. Does anyone know what is going on?
After much trial and error I determined the problem. I forgot that I had setup a FutureBuilder for the / route in my MaterialApp widget. I was passing a function call that returns a future to the future parameter of the FutureBuilder constructor rather than a variable pointing to a future.
So every time the routes got updated a brand new future was being created. Doing the function call outside of the MaterialApp constructor and storing the resulting future in a variable, then passing that to the FutureBuilder did the trick.
It doesn't seem like this would be connected to the weird behavior I was getting when a keyboard opened, but it was definitely the cause. See below for what I mean.
Code with a bug:
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.teal,
accentColor: Colors.tealAccent,
buttonColor: Colors.lightBlue,
),
routes: {
'/': (context) => FutureBuilder<void>(
future: futureFun(), //Bug! I'm passing a function that returns a future when called. So a new future is returned each time
builder: (context, snapshot) {
...
}
...
}
...
}
Fixed Code:
final futureVar = futureFun(); //calling the function here instead and storing its future in a variable
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.teal,
accentColor: Colors.tealAccent,
buttonColor: Colors.lightBlue,
),
routes: {
'/': (context) => FutureBuilder<void>(
future: futureVar, //Fixed! Passing the reference to the future rather than the function call
builder: (context, snapshot) {
...
}
...
}
...
}
did you use AutomaticKeepAliveClientMixin in "A" widget ?
if you don't , see this https://stackoverflow.com/a/51738269/3542938
if you already use it , please give us a code that we can test it directly into "main.dart" to help you
Yup, happened to me, perhaps it's much better to wrap the FutureBuilder itu a PageWidget, and make it singleton
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.teal,
accentColor: Colors.tealAccent,
buttonColor: Colors.lightBlue,
),
routes: {
'/': (context) => PageWidget() // wrap it by PageWidget
...
}
...
}
class PageWidget extends StatelessWidget {
static final _instance = PageWidget._internal(); // hold instance
PageWidget._internal(); // internal consturctor
factory PageWidget() {
return _instance; // make it singleton
}
#override
Widget build(BuildContext context) {
return FutureBuilder<void>( ... );
}
}
I got a solution, I was initialising variables in the constructor of the superclass. I removed it and worked!
I just removed the FutureBuilder from the home of MaterialApp and changed the MyApp into a Stateful widget and fetched the requisite info in the initState and called setState in the .then(); of the future and instead of passing multiple conditions in the home of MaterialApp, I moved those conditions to a separate Stateful widget and the issue got resolved.
initState:
#override
void initState() {
// TODO: implement initState
// isSignedIn = SharedPrefHelper.getIsSignedIn();
getIsSignedInFromSharedPreference().then((value) {
setState(() {
isSignedInFromSharedPref = value ?? false;
if (isSignedInFromSharedPref) {
merchantKey = LocalDatabase.getMerchantKeyWithoutAsync();
}
isLoadingSharedPrefValue = false;
});
});
super.initState();
}
Future<bool?> getIsSignedInFromSharedPreference() async {
return SharedPrefHelper.getIsSignedIn();
}
MaterialApp (now):
MaterialApp(
title: 'Loveeatry POS',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Home(
isLoadingSharedPrefValue: isLoadingSharedPrefValue,
isSignedInFromSharedPref: isSignedInFromSharedPref,
merchantKey: merchantKey,
),
),
Home:
class Home extends StatelessWidget {
final bool isLoadingSharedPrefValue;
final bool isSignedInFromSharedPref;
final String merchantKey;
const Home({
Key? key,
required this.isLoadingSharedPrefValue,
required this.isSignedInFromSharedPref,
required this.merchantKey,
}) : super(key: key);
#override
Widget build(BuildContext context) {
if (!isLoadingSharedPrefValue) {
if (isSignedInFromSharedPref) {
return const Homepage(
shouldLoadEverything: true,
);
} else if (merchantKey.isNotEmpty) {
return LoginPage(merchantKey: merchantKey);
} else {
return const AddMerchantKeyPage();
}
} else {
return loading(context);
}
}
}
P.S.: If you need any more info, please leave a comment.