Making Private Route in Flutter - flutter

How can I make a wrapper over my private routes, which navigate to screen only when user is authorized, otherwise redirect to login and get back to the original screen after login.
How can make this in a generalized way, so that I just reuse it on my other Private future screens?

If you are using routes parameter in your MaterialApp, you can replace it with following implementation
import 'dart:collection';
import 'package:flutter/widgets.dart';
class ConditionalRouter extends MapMixin<String, WidgetBuilder> {
final Map<String, WidgetBuilder> public;
final Map<String, WidgetBuilder> private;
ConditionalRouter({this.public, this.private});
#override
WidgetBuilder operator [](Object key) {
if (public.containsKey(key))
return public[key];
if (private.containsKey(key)) {
if (MyAuth.isUserLoggedIn)
return private[key];
// Adding next page parameter to your Login page
// will allow you to go back to page, that user were going to
return (context) => LoginPage(nextPage: key);
}
return null;
}
#override
void operator []=(key, value) {}
#override
void clear() {}
#override
Iterable<String> get keys {
final set = Set<String>();
set.addAll(public.keys);
set.addAll(private.keys);
return set;
}
#override
WidgetBuilder remove(Object key) {
return public[key] ?? private[key];
}
}
And use it like that:
MaterialApp(
// ...
routes: ConditionalRouter(
public: {
'/start_page': (context) => StartPage()
},
private: {
'/user_profile': (context) => UserProfilePage()
}
)
)

Use StreamBuilder widget and provide it a Stream of access token/uid if there is no data then return login screen and when user is authenticated then put access token into the stream that will rebuild the StreamBuilder which return the page when user is authenticated.
Use bloc pattern or bloc library for better state management.
Hope this will help you.

Generalised idea, you could make the controller a Static variable
class LoginController{
final Function onContactingServerDone;
LoginController({this.onContactingServerDone,});
bool loggedIn;
login()async {
//contact server,get token or verify token etc
onContactingServerDone();
}
}
and in your screen
LoginController _loginController;
initState(){
_loginController = LoginController(onContactingServerDone: (){
if(_loginController.loggedIn){
Navigator.of(context).pushNamed('privateRoute');
} else {
Navigator.of(context).pushNamed('login');
}
},);
_loginController.login();
}
Widget build(context){
return CircularProgressIndicator();
}

Related

Flutter GetX state management initial null value

This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :

How to get top most screens route name

I am following this Navigate with named routes. Where I am passing a route name to view the new screen.
Navigator.pushNamed(context, '/second');
So, what I need is the topmost screens routeName. in this case /second.
How can I get it?
other than Modal.of(context), flutter doesn't provide any easier way to get the latest route name.
but anyone who is using route handling using onRouteGenerated property can do this
class AllRoutes{
static String _lastRoute = "/";
static Route onGenerated(RouteSettings settings){
_lastRoute = settings.name;
//handle route changes here rather passing the route Map to the App
}
static String get lastRoute=>_lastRoute;
}
and add the static method as the onGenerateRoute property value of the App
MaterialApp(
onGenerateRoute: AllRoutes.onGenerated,
)
and get the last route like this
var lastRoute = AllRoutes.lastRoute
You could add this as a extension to Navigator so you could feel at home ;)
String? getTopRouteName (BuildContext context) {
String? top;
Navigator.popUntil(context, (route) {
top = route.settings.name;
return true;
});
return top;
}
Unfortunately, Navigator does not provide such API. But the top most route name could be found through this tricky method, which is trying to pop the top most route, but prevented by the return value true.
You can register as an observer in navigatorObservers in MaterialApp to get notified when routes change, and keep the last one:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: const MyHomePage(),
navigatorObservers: [
RouteObserver(),
],
);
}
}
class RouteObserver extends NavigatorObserver {
String? lastRoute;
#override
void didReplace({Route? newRoute, Route? oldRoute}) {
lastRoute = newRoute?.settings.name;
}
#override
void didPush(Route route, Route? previousRoute) {
lastRoute = route.settings.name;
}
#override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
lastRoute = previousRoute?.settings.name;
}
#override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
lastRoute = previousRoute?.settings.name;
}
}

Flutter what is the best approach to use navigator and ChangeNotifierProvider together

I'm new to flutter, this question may be a very basic one.
I have a firebase phone auth login page to implement this,
if the user is logged in, then navigate to home page
else if the user is a new user, then navigate to the sign-up page
The problem is, whenever the values are changed at the provider, the consumer will get notified and rebuild the build method. I won't be able to listen to them within the build method and return a Navigator.of(context).pushNamed(). Any idea what is the right way to use ChangeNotifierProvider along with listeners and corresponding page navigation?
I have Login class and provider class as below,
class LoginPage extends StatefulWidget {
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => LoginProvider(),
child: Consumer<LoginProvider>(builder: (context, loginState, child) {
return Scaffold(
...
body: RaisedButton(
onPressed: **loginState.doLogin(_textController.text, context);**
...
)
}),
);
}
}
class LoginProvider with ChangeNotifier {
bool _navigateToSignup = false;
bool get navigateToSignup => _navigateToSignup;
Future doLogin(String mobile, BuildContext context) async {
FirebaseAuth _auth = FirebaseAuth.instance;
_auth.verifyPhoneNumber(
...
verificationCompleted: (AuthCredential credential) async {
UserCredential result = await _auth.signInWithCredential(credential);
User user = result.user;
// if user is new user navigate to signup
// do not want to use Navigator.of(context).pushNamed('/signupPage'); here, instead would like to notify listeners at login page view and then use navigator.
if (user.metadata.creationTime == user.metadata.lastSignInTime) {
_navigateToSignup = true;
} else {
if (result.user != null) {
_navigateToHome = true;
//Navigator.of(context).pushNamedAndRemoveUntil('/homePage', ModalRoute.withName('/'));
}
}
notifyListeners();
},
...
);
}
}
Thanks in advance.
There are several approaches, you choose the one that suits you best.
Pass the context to the ChangeNotifier as you are already doing. I don't like this as well, but some people do it.
Pass a callback to your ChangeNotifier that will get called when you need to navigate. This callback will be executed by your UI code.
Same as 2, but instead of a callback export a Stream and emit an event indicating you need to Navigate. Then you just listen to that Stream on your UI and navigate from there.
Use a GlobalKey for your Navigator and pass it to your MaterialApp, than you can use this key everywhere. More details here.

How to pop screen using Mobx in flutter

I have a Food object that contains properties like name, id, calories, etc. With a series of screens, the user populates the food object properties.
Once done, the user can press the submit button, that will call the addFood method in the store.
The problem is, after uploading the food to the server, i want to pop the screen or show error message in toast based on the response. I just don't know how to do this.
Following is my code (only the important bits):
FoodDetailStore.dart
class FoodDetailStore = _FoodDetailStore with _$FoodDetailStore;
abstract class _FoodDetailStore with Store {
Repository _repository;
Food _food;
#observable
String msg = '';
// ... Other Observables and actions
#action
addFood(bool toAdd) {
if (toAdd) {
_repository.addFood(food).then((docId) {
if (docId != null) {
// need to pop the screen
}
}).catchError((e) {
// show error to the user.
// I tried this, but it didn't work
msg = 'there was an error with message ${e.toString()}. please try again.';
});
}
// .. other helper methods.
}
FoodDetailScreen.dart (Ignore the bloc references, I am currently refactoring code to mobx)
class FoodDataScreen extends StatefulWidget {
final String foodId;
final Serving prevSelectedServing;
final bool fromNewRecipe;
FoodDataScreen({#required this.foodId, this.prevSelectedServing, this.fromNewRecipe});
#override
_FoodDataScreenState createState() => _FoodDataScreenState(
this.foodId,
this.prevSelectedServing,
this.fromNewRecipe,
);
}
class _FoodDataScreenState extends State<FoodDataScreen> {
final String foodId;
final Serving prevSelectedServing;
final bool fromNewRecipe;
FoodDataBloc _foodDataBloc;
_FoodDataScreenState(
this.foodId,
this.prevSelectedServing,
this.fromNewRecipe,
);
FoodDetailStore store;
#override
void initState() {
store = FoodDetailStore();
store.initReactions();
store.initializeFood(foodId);
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
// I know this is silly, but this is what i tried. Didn't worked
Observer(
builder: (_) {
_showMsg(store.msg);
}
);
}
#override
Widget build(BuildContext context) {
return Container(
// ... UI
);
}
_popScreen() {
_showMsg('Food Added');
Majesty.router.pop(context);
}
_showMsg(String msg) {
Fluttertoast.showToast(msg: msg);
}
#override
void dispose() {
store.dispose();
super.dispose();
}
}
Constructing an Observer instance inside the didChangeDependencies() is indeed "silly" as you have rightly noted already :)
Observer is a widget and widget needs to be inserted into the widgets tree in order to do something useful. In our case non-widget Mobx reactions come to the rescue.
I will show how I did it in my code for the case of showing a Snackbar upon observable change so you will get an idea how to transform your code.
First of all, import import 'package:mobx/mobx.dart';.
Then in the didChangeDependencies() create a reaction which will use some of your observables. In my case these observables are _authStore.registrationError and _authStore.loggedIn :
final List<ReactionDisposer> _disposers = [];
#override
void dispose(){
_disposers.forEach((disposer) => disposer());
super.dispose();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_authStore = Provider.of<AuthStore>(context);
_disposers.add(
autorun(
(_) {
if (_authStore.registrationError != null)
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text(_authStore.registrationError),
backgroundColor: Colors.redAccent,
duration: Duration(seconds: 4),
),
);
},
),
);
_disposers.add(
reaction(
(_) => _authStore.loggedIn,
(_) => Navigator.of(context).pop(),
),
);
}
I use two types of Mobx reactions here: autorun and reaction. autorun triggers the first time immediately after you crate it and then every time the observable changes its value. reaction does not trigger the first time, only when the observable change.
Also pay attention to dispose the created reactions in the dispose() method to avoid resources leak.
Here is a code of my Mobx store class with used observables to complete the picture:
import 'package:mobx/mobx.dart';
import 'dart:convert';
part "auth_store.g.dart";
class AuthStore = AuthStoreBase with _$AuthStore;
abstract class AuthStoreBase with Store{
#observable
String token;
#observable
String registrationError;
#observable
String loginError;
#action
void setToken(String newValue){
token = newValue;
}
#action
void setRegistrationError(String newValue){
registrationError = newValue;
}
#action
void setLoginError(String newValue){
loginError = newValue;
}
#action
void resetLoginError(){
loginError = null;
}
#computed
bool get loggedIn => token != null && token.length > 0;
#action
Future<void> logOut() async{
setToken(null);
}
}

Flutter - How to pass user data to all views

I'm new to the flutter world and mobile app development and struggling with how I should pass user data throughout my app.
I've tried several things, but none seem great and I'm sure there are best practice patterns I should be following.
Because it makes examples easier, I'm using firebase for authentication.
I currently have a separate route for logging in. Once I'm logged in I want the User model in most views for checking permissions on what to show, displaying user info in the drawer, etc...
Firebase has an await firebaseAuth.currentUser(); Is it best practice to call this everywhere you might need the user? and if so, where is the best spot to place this call?
The flutter codelab shows a great example of authenticating users before allowing writes. However, if the page needs to check auth to determine what to build, the async call can't go in the build method.
initState
One method I've tried is to override initState and kick off the call to get the user. When the future completes I call setState and update the user.
FirebaseUser user;
#override
void initState() {
super.initState();
_getUserDetail();
}
Future<Null> _getUserDetail() async {
User currentUser = await firebaseAuth.currentUser();
setState(() => user = currentUser);
}
This works decent but seems like a lot of ceremony for each widget that needs it. There is also a flash when the screen loads without the user and then gets updated with the user upon the future's completion.
Pass the user through the constructor
This works too but is a lot of boilerplate to pass the user through all routes, views, and states that might need to access them. Also, we can't just do popAndPushNamed when transitioning routes because we can't pass a variable to it. We have to change routes similar to this:
Navigator.push(context, new MaterialPageRoute(
builder: (BuildContext context) => new MyPage(user),
));
Inherited Widgets
https://medium.com/#mehmetf_71205/inheriting-widgets-b7ac56dbbeb1
This article showed a nice pattern for using InheritedWidget. When I place the inherited widget at the MaterialApp level, the children aren't updating when the auth state changed (I'm sure I'm doing it wrong)
FirebaseUser user;
Future<Null> didChangeDependency() async {
super.didChangeDependencies();
User currentUser = await firebaseAuth.currentUser();
setState(() => user = currentUser);
}
#override
Widget build(BuildContext context) {
return new UserContext(
user,
child: new MaterialApp(
title: 'TC Stream',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new LoginView(title: 'TC Stream Login', analytics: analytics),
routes: routes,
),
);
}
FutureBuilder
FutureBuilder also seems like a decent option but seems to be a lot of work for each route. In the partial example below, _authenticateUser() is getting the user and setting state upon completion.
#override
Widget build(BuildContext context) {
return new FutureBuilder<FirebaseUser>(
future: _authenticateUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildProgressIndicator();
}
if (snapshot.connectionState == ConnectionState.done) {
return _buildPage();
}
},
);
}
I'd appreciate any advice on best practice patterns or links to resources to use for examples.
I'd recommend investigating inherited widgets further; the code below shows how to use them with asynchronously updating data:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(new MaterialApp(
title: 'Inherited Widgets Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: new Text('Inherited Widget Example'),
),
body: new NamePage())));
}
// Inherited widget for managing a name
class NameInheritedWidget extends InheritedWidget {
const NameInheritedWidget({
Key key,
this.name,
Widget child}) : super(key: key, child: child);
final String name;
#override
bool updateShouldNotify(NameInheritedWidget old) {
print('In updateShouldNotify');
return name != old.name;
}
static NameInheritedWidget of(BuildContext context) {
// You could also just directly return the name here
// as there's only one field
return context.inheritFromWidgetOfExactType(NameInheritedWidget);
}
}
// Stateful widget for managing name data
class NamePage extends StatefulWidget {
#override
_NamePageState createState() => new _NamePageState();
}
// State for managing fetching name data over HTTP
class _NamePageState extends State<NamePage> {
String name = 'Placeholder';
// Fetch a name asynchonously over HTTP
_get() async {
var res = await http.get('https://jsonplaceholder.typicode.com/users');
var name = json.decode(res.body)[0]['name'];
setState(() => this.name = name);
}
#override
void initState() {
super.initState();
_get();
}
#override
Widget build(BuildContext context) {
return new NameInheritedWidget(
name: name,
child: const IntermediateWidget()
);
}
}
// Intermediate widget to show how inherited widgets
// can propagate changes down the widget tree
class IntermediateWidget extends StatelessWidget {
// Using a const constructor makes the widget cacheable
const IntermediateWidget();
#override
Widget build(BuildContext context) {
return new Center(
child: new Padding(
padding: new EdgeInsets.all(10.0),
child: const NameWidget()));
}
}
class NameWidget extends StatelessWidget {
const NameWidget();
#override
Widget build(BuildContext context) {
final inheritedWidget = NameInheritedWidget.of(context);
return new Text(
inheritedWidget.name,
style: Theme.of(context).textTheme.display1,
);
}
}
I prefer to use Services with Locator, using Flutter get_it.
Create a UserService with a cached data if you like:
class UserService {
final Firestore _db = Firestore.instance;
final String _collectionName = 'users';
CollectionReference _ref;
User _cachedUser; //<----- Cached Here
UserService() {
this._ref = _db.collection(_collectionName);
}
User getCachedUser() {
return _cachedUser;
}
Future<User> getUser(String id) async {
DocumentSnapshot doc = await _ref.document(id).get();
if (!doc.exists) {
log("UserService.getUser(): Empty companyID ($id)");
return null;
}
_cachedUser = User.fromDocument(doc.data, doc.documentID);
return _cachedUser;
}
}
Then create create a Locator
GetIt locator = GetIt.instance;
void setupLocator() {
locator.registerLazySingleton(() => new UserService());
}
And instantiate in main()
void main() {
setupLocator();
new Routes();
}
That's it! You can call your Service + cachedData everywhere using:
.....
UserService _userService = locator<UserService>();
#override
void initState() {
super.initState();
_user = _userService.getCachedUser();
}
I crashed into another problem because of this problem you can check it out here
So the solution I came up with is a bit untidy,I created a separate Instance dart page and imported it to every page.
GoogleSignInAccount Guser = googleSignIn.currentUser;
FirebaseUser Fuser;
I stored the user there on login and checked on every StateWidget if it was null
Future<Null> _ensureLoggedIn() async {
if (Guser == null) Guser = await googleSignIn.signInSilently();
if (Fuser == null) {
await googleSignIn.signIn();
analytics.logLogin();
}
if (await auth.currentUser() == null) {
GoogleSignInAuthentication credentials =
await googleSignIn.currentUser.authentication;
await auth.signInWithGoogle(
idToken: credentials.idToken,
accessToken: credentials.accessToken,
);
}
This is my old code I did cleaned it up on my current app but I don't have that code now in handy. Just check out for null user and log it in again
I did it for most of the Firebase instances too because I have more than 3 pages on my app and Inherited Widgets was just too much work
You can use the GetX package to check whether or not the user is logged in, get user data and have it accessible throughout your app
For my lazy mathod,
i just create new file like userdata.dart and then put any variable on it for example like dynamic Profile = null
inside userdata.dart
//only put this or anything u want.
dynamic Profile = null;
at startingpage.dart
//import that file
import '../userdata.dart';
class startingpage extends ...{
...
//set data to store..
Profile = 'user profile';
...
}
to use the data just declare and use in
anotherpage.dart
//import that file
import '../userdata.dart';
class anotherpage extends...{
...
}
class .. State ...{
...
//set the data to variable
dynamic userdata = Profile;
print('this is my lazy pass data' + userdata.toString());
...
}