static switchTheme({required ThemeBase newTheme}) {
VariableUtilities.theme = newTheme;
switch (newTheme) {
case LightTheme():
ThemeBase theme = const LightTheme();
return VariableUtilities.prefs
.setString(LocalCacheKey.applicationThemeMode, theme.toString());
case DarkTheme():
ThemeBase theme = const DarkTheme();
return VariableUtilities.prefs
.setString(LocalCacheKey.applicationThemeMode, theme.toString());
default:
ThemeBase theme = const LightTheme();
return VariableUtilities.prefs
.setString(LocalCacheKey.applicationThemeMode, theme.toString());
}
}
you can change theme like this. it's from flutter skeleton template.
it can change base of platform theme mode too.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Set up the SettingsController, which will glue user settings to multiple
// Flutter Widgets.
final settingsController = SettingsController(SettingsService());
// Load the user's preferred theme while the splash screen is displayed.
// This prevents a sudden theme change when the app is first displayed.
await settingsController.loadSettings();
// Run the app and pass in the SettingsController. The app listens to the
// SettingsController for changes, then passes it further down to the
// SettingsView.
runApp(MyApp(settingsController: settingsController));
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.settingsController,
}) : super(key: key);
final SettingsController settingsController;
#override
Widget build(BuildContext context) {
// Glue the SettingsController to the MaterialApp.
//
// The AnimatedBuilder Widget listens to the SettingsController for changes.
// Whenever the user updates their settings, the MaterialApp is rebuilt.
return AnimatedBuilder(
animation: settingsController,
builder: (BuildContext context, Widget? child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
restorationScopeId: 'MyApp',
theme: AppTheme.newLightTheme(),
darkTheme: AppTheme.newDarkTheme(),
themeMode: settingsController.themeMode,
onGenerateRoute: (RouteSettings routeSettings) {
return MaterialPageRoute<void>(
settings: routeSettings,
builder: (BuildContext context) {
switch (routeSettings.name) {
case MainScreen.routeName:
return MainScreen(settingsController: settingsController);
default:
return const ChooseLogin();
}
},
);
},
);
},
);
}
}
settings_controller.dart
/// A class that many Widgets can interact with to read user settings, update
/// user settings, or listen to user settings changes.
///
/// Controllers glue Data Services to Flutter Widgets. The SettingsController
/// uses the SettingsService to store and retrieve user settings.
class SettingsController with ChangeNotifier {
SettingsController(this._settingsService);
// Make SettingsService a private variable so it is not used directly.
final SettingsService _settingsService;
// Make ThemeMode a private variable so it is not updated directly without
// also persisting the changes with the SettingsService.
late ThemeMode _themeMode;
// Allow Widgets to read the user's preferred ThemeMode.
ThemeMode get themeMode => _themeMode;
/// Load the user's settings from the SettingsService. It may load from a
/// local database or the internet. The controller only knows it can load the
/// settings from the service.
Future<void> loadSettings() async {
_themeMode = await _settingsService.themeMode();
// Important! Inform listeners a change has occurred.
notifyListeners();
}
/// Update and persist the ThemeMode based on the user's selection.
Future<void> updateThemeMode(ThemeMode? newThemeMode) async {
if (newThemeMode == null) return;
// Do not perform any work if new and old ThemeMode are identical
if (newThemeMode == _themeMode) return;
// Otherwise, store the new ThemeMode in memory
_themeMode = newThemeMode;
// Important! Inform listeners a change has occurred.
notifyListeners();
// Persist the changes to a local database or the internet using the
// SettingService.
await _settingsService.updateThemeMode(newThemeMode);
}
}
settings_services.dart
/// A service that stores and retrieves user settings.
///
/// By default, this class does not persist user settings. If you'd like to
/// persist the user settings locally, use the shared_preferences package. If
/// you'd like to store settings on a web server, use the http package.
class SettingsService {
static const String kPrefKeyThemeMode = 'ThemeMode';
/// Loads the User's preferred ThemeMode from local or remote storage.
Future<ThemeMode> themeMode() async {
SharedPreferences _prefs = await SharedPreferences.getInstance();
String _name = _prefs.getString(kPrefKeyThemeMode) ?? ThemeMode.system.name;
if (_name == ThemeMode.light.name) {
return ThemeMode.light;
} else if (_name == ThemeMode.dark.name) {
return ThemeMode.dark;
} else {
return ThemeMode.system;
}
}
/// Persists the user's preferred ThemeMode to local or remote storage.
Future<void> updateThemeMode(ThemeMode theme) async {
// Use the shared_preferences package to persist settings locally or the
// http package to persist settings over the network.
SharedPreferences _prefs = await SharedPreferences.getInstance();
_prefs.setString(kPrefKeyThemeMode, theme.name);
}
}
settings_page.dart
class SettingsPage extends StatelessWidget {
const SettingsPage({
Key? key,
required this.controller,
}) : super(key: key);
static const routeName = '/settings';
final SettingsController controller;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: Padding(
padding: const EdgeInsets.all(16),
// Glue the SettingsController to the theme selection DropdownButton.
//
// When a user selects a theme from the dropdown list, the
// SettingsController is updated, which rebuilds the MaterialApp.
child: DropdownButton<ThemeMode>(
// Read the selected themeMode from the controller
value: controller.themeMode,
// Call the updateThemeMode method any time the user selects a theme.
onChanged: controller.updateThemeMode,
items: const [
DropdownMenuItem(
value: ThemeMode.system,
child: Text('System Theme'),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text('Light Theme'),
),
DropdownMenuItem(
value: ThemeMode.dark,
child: Text('Dark Theme'),
)
],
),
),
);
}
}
Related
i want to be able to call an empty variable from a class, assign a value to it and make it persistent, anything aside provider e.t.c would be help, i don't want to overhaul the entire app again to do some bloc, provider e.t.c
NB: all screens are stateful widgets
i have tried creating a class with an empty string and passing a value to it from another screen, but this doesn't seem to work
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
class MethodA {
// id(user, context){
// var name =user.email;
// }
String identity;
MethodA({this.iD});
bool isLoggedIn() {
if (FirebaseAuth.instance.currentUser() != null) {
return true;
} else {
return false;
}
}
Future<void> addUserA( userinfo) async {
//this.iD=id;
Firestore.instance
.collection('user')
.document('furtherinfo').collection(identity).document('Personal Info')
.setData(userdoc)
.catchError((e) {
print(e);
});
}
each time i pass the argument to i.e foo='bar';
and i import that class in another screen, i.e screen 9, foo is automatically set to null, but i would want foo to be bar
I would suggest that you use the Provider since it is the easiest way for me to manage state throughout the app. Flutter starts with one component on top of the widget tree so i would place my provider here.
Example
void main() {runApp(MyApp());}
class MyApp extends StatelessWidget {
MyApp();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged, // Provider to manage user throughout the app.
),
],
child: MaterialApp(
title: 'My App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.green,
primarySwatch: Colors.green,
accentColor: Colors.yellow,
),
home: MainPage(),
),
);
}
}
Then in your class you can do the following
class MethodAService with ChangeNotifier {
String _identity = null;
FirebaseUser _user = null;
// constructor with the (new changes )
MethodAService(FirebaseUser user){
this._user = user;
}
get identity => _identity ;
setIdentity(String identity) {
_identity = identity ;
notifyListeners(); // required to notify the widgets of your change
}
}
Then when you want to use it anywhere in your app just do the following in the build method
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context); // to get the current user
final methodA = Provider.of<MethodAService>(context); // get your service with identity
// now you can set the string using
methodA.setIdentity('new identity');
// or just use it like this
if(methodA.identity.isNotEmpty()){
print(methodA.identity);
}else{
print('Identity is empty');
}
return ChangeNotifierProvider<MethodAService>(
builder: (context) => MethodAService(user), // Your provider to manage your object, sending the Firebase user in
child: loggedIn ? HomePage() : LoginPage(), );
}
References
Provider Package
Fireship 185 Provider
Great Youtube video explaining the code
Update for comment
For getting the user uid you can just do user.uid
Changed code above to fit the
I'm not sure put the whole app in a StreamProvider is the best choice. That means the app will be rebuilt on each stream value.
To make a Widget available on all screens, you need a TransitionBuilder in your MaterialApp.
To avoid the external dependency you can also use an InheritedWidget
signed_user.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class SignedUser extends InheritedWidget {
final FirebaseUser user;
SignedUser({#required this.user, #required Widget child})
: super(child: child);
#override
bool updateShouldNotify(SignedUser oldWidget) => true;
static SignedUser of(BuildContext context) =>
context.inheritFromWidgetOfExactType(SignedUser);
}
my_transition_builder.dart
class MyTransitionBuilder extends StatefulWidget {
final Widget child;
const MyTransitionBuilder({Key key, this.child}) : super(key: key);
#override
_MyTransitionBuilderState createState() => _MyTransitionBuilderState();
}
class _MyTransitionBuilderState extends State<MyTransitionBuilder> {
StreamBuilder<FirebaseUser> _builder;
#override
void initState() {
super.initState();
_builder = StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
return SignedUser(
child: widget.child,
user: snapshot.data,
);
});
}
#override
Widget build(BuildContext context) {
return _builder;
}
}
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
// this will make your inherited widget available on all screens of your app
builder: (context, child) {
return MyTransitionBuilder(child: child);
},
routes: {
'/editAccount': (context) => new EditAccountPage(),
},
theme: ThemeData(
primarySwatch: Colors.green,
),
home: MyHomePage(),
);
}
}
usage in edit_account_page.dart
#override
Widget build(BuildContext context) {
var user = SignedUser.of(context).user;
return Scaffold(
body: FutureBuilder<DocumentSnapshot>(
future: Firestore.instance.document('users/${user.uid}').get(),
I've been trying without success to load different pages according to my Shared Preference settings.
Based on several posts found in stackoverflow, i end up with the following solution:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:testing/screens/login.dart';
import 'package:testing/screens/home.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget page = Login();
Future getSharedPrefs() async {
String user = Preferences.local.getString('user');
if (user != null) {
print(user);
this.page = Home();
}
}
#override
void initState() {
super.initState();
this.getSharedPrefs();
}
#override
Widget build(BuildContext context) {
return MaterialApp(home: this.page);
}
}
class Preferences {
static SharedPreferences local;
/// Initializes the Shared Preferences and sets the info towards a global variable
static Future init() async {
local = await SharedPreferences.getInstance();
}
}
The variable user is not null because the print(user) returns a value as expected, but the login screen is always being opened.
Your problem is that your build method returns before your getSharedPrefs future is complete. The getSharedPrefs returns instantly as soon as it's called because it's async and you're treating it as a "Fire and Forget" by not awaiting. Seeing that you can't await in your initState function that makes sense.
This is where you want to use the FutureBuilder widget. Create a Future that returns a boolean (or enum if you want more states) and use a future builder as your home child to return the correct widget.
Create your future
Future<bool> showLoginPage() async {
var sharedPreferences = await SharedPreferences.getInstance();
// sharedPreferences.setString('user', 'hasuser');
String user = sharedPreferences.getString('user');
return user == null;
}
When user is null this will return true. Use this future in a Future builder to listen to the value changes and respond accordingly.
#override
Widget build(BuildContext context) {
return MaterialApp(home: FutureBuilder<bool>(
future: showLoginPage(),
builder: (buildContext, snapshot) {
if(snapshot.hasData) {
if(snapshot.data){
// Return your login here
return Container(color: Colors.blue);
}
// Return your home here
return Container(color: Colors.red);
} else {
// Return loading screen while reading preferences
return Center(child: CircularProgressIndicator());
}
},
));
}
I ran this code and it works fine. You should see a blue screen when login is required and a red screen when there's a user present. Uncomment the line in showLoginPage to test.
There is a much pretty way of doing this.
Assuming that you have some routes and a boolean SharedPreference key called initialized.
You need to use the WidgetsFlutterBinding.ensureInitialized() function before calling runApp() method.
void main() async {
var mapp;
var routes = <String, WidgetBuilder>{
'/initialize': (BuildContext context) => Initialize(),
'/register': (BuildContext context) => Register(),
'/home': (BuildContext context) => Home(),
};
print("Initializing.");
WidgetsFlutterBinding.ensureInitialized();
await SharedPreferencesClass.restore("initialized").then((value) {
if (value) {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Home(),
);
} else {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Initialize(),
);
}
});
print("Done.");
runApp(mapp);
}
The SharedPreference Class Code :
class SharedPreferencesClass {
static Future restore(String key) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
return (sharedPrefs.get(key) ?? false);
}
static save(String key, dynamic value) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
if (value is bool) {
sharedPrefs.setBool(key, value);
} else if (value is String) {
sharedPrefs.setString(key, value);
} else if (value is int) {
sharedPrefs.setInt(key, value);
} else if (value is double) {
sharedPrefs.setDouble(key, value);
} else if (value is List<String>) {
sharedPrefs.setStringList(key, value);
}
}
}
I am using sqflite database to save user list.
I have user list screen, which shows list of user and it has a fab button,
on click of fab button, user is redirected to next screen where he can add new user to database.
The new user is properly inserted to the database
but when user presses back button and go backs to user list screen,
the newly added user is not visible on the screen.
I have to close the app and reopen it,then the newly added user is visible on the screen.
I am using bloc pattern and following is my code to show user list
class _UserListState extends State<UserList> {
UserBloc userBloc;
#override
void initState() {
super.initState();
userBloc = BlocProvider.of<UserBloc>(context);
userBloc.fetchUser();
}
#override
void dispose() {
userBloc?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed("/detail");
},
child: Icon(Icons.add),
),
body: StreamBuilder(
stream: userBloc.users,
builder: (context, AsyncSnapshot<List<User>> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data != null) {
return ListView.builder(
itemBuilder: (context, index) {
return Dismissible(
key: Key(snapshot.data[index].id.toString()),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
userBloc.deleteParticularUser(snapshot.data[index]);
},
child: ListTile(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => UserDetail(
user: snapshot.data[index],
)));
},
title: Text(snapshot.data[index].name),
subtitle:
Text("Mobile Number ${snapshot.data[index].userId}"),
trailing:
Text("User Id ${snapshot.data[index].mobileNumber}"),
),
);
},
itemCount: snapshot.data.length,
);
}
},
),
);
}
}
Following is my bloc code
class UserBloc implements BlocBase {
final _users = BehaviorSubject<List<User>>();
Observable<List<User>> get users => _users.stream;
fetchUser() async {
await userRepository.initializeDatabase();
final users = await userRepository.getUserList();
_users.sink.add(users);
}
insertUser(String name,int id,int phoneNumber) async {
userRepository.insertUser(User(id, name, phoneNumber));
fetchUser();
}
updateUser(User user) async {
userRepository.updateUser(user);
}
deleteParticularUser(User user) async {
userRepository.deleteParticularUser(user);
}
deleteAllUser() {
return userRepository.deleteAllUsers();
}
#override
void dispose() {
_users.close();
}
}
As Remi posted answer saying i should try BehaviorSubject and ReplaySubject which i tried but it does not help. I have also called fetchUser(); inside insertUser() as pointed in comments
Following is the link of the full example
https://github.com/pritsawa/sqflite_example
Follow up from the comments, it seems you don't have a single instance of your UsersBloc in those two pages. Both the HomePage and UserDetails return a BlocProvider which instantiate a UsersBloc instance. Because you have two blocs instances(which you shouldn't have) you don't update the streams properly.
The solution is to remove the BlocProvider from those two pages(HomePage and UserDetail) and wrap the MaterialApp widget in it to make the same instance available to both pages.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: UserBloc(),
child:MaterialApp(...
The HomePage will be:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return UserList(),
);
}
}
Remove the BlocProvider from UserDetail as well.
In the UsersBloc then call fetchUser() inside the insertUser() method after the user insertion, to requery the database and update the users stream.
Also as Rémi Rousselet said, use one of the subjects that return previous values.
The issue is that you're using a PublishSubject.
When a new listener subscribes to a PublishSubject, it does not receive the previously sent value and will only receive the next events.
The easiest solution is to use a BehaviorSubject or a ReplaySubject instead. These two will directly call their listener with the latest values.
#override
void initState() {
super.initState();
userBloc = BlocProvider.of<UserBloc>(context);
userBloc.fetchUser();
}
The problem is that you have called the userBloc.fetchUser() function in the initState of the page.
Bloc stream emits whenever a new data is added to it and the userBloc.fetchUser() function does exactly that, it adds the userList that you fetch from the Sqflite database.
Whenever you come back to the userlist screen from add user screen, init function is NOT called. It is only called when the userlist screen is created, that is, whenever you push it to the navigation stack.
The workaround is to call userBloc.fetchUser() whenever your StreamBuilder's snapshot data is null.
...
if (!snapshot.hasData) {
userBloc.fetchUser();
return Center(
child: CircularProgressIndicator(),
);
}
...
I want to do a login app. I have a class user, which has an id and a username that I want to keep to display it later in the app, and I have a user_api class, where I do the http request.
I wanted to use Singleton to store the user once the user logins in, but I find out that inherited widget was a better idea. So now I'm struggling with them because I can't store the user object. After I login, my user becomes null and I can't figure out how it works. Here's my code: basically we have a root page that manages the cases in which the user is logged in or not:
void main() {
runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Model(
user: User(),
child: MaterialApp(
routes: {
'/login': (context) => new LoginView(),
'/homepage_view': (context) => new HomepageView(),
},
title: 'Flutter login demo',
home: RootPage(),
),
);
}
}
In the rootPage:
enum UserStatus {
notDetermined,
notSignedIn,
signedIn,
}
class RootPage extends StatefulWidget {
#override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
UserStatus userStatus = UserStatus.notDetermined;
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
var user = Model.of(context).user;
setState(() {
userStatus = user.id == null? UserStatus.notSignedIn : UserStatus.signedIn;
print((userStatus));
});
}
void _signedIn() {
setState(() {
userStatus = UserStatus.signedIn;
});
}
void _signedOut() {
setState(() {
userStatus = UserStatus.notSignedIn;
});
}
#override
Widget build(BuildContext context) {
switch (userStatus) {
case UserStatus.notDetermined:
return _buildWaitingScreen();
case UserStatus.notSignedIn:
return LoginView(
onSignedIn: _signedIn,
);
case UserStatus.signedIn:
return HomepageView(
onSignedOut: _signedOut,
);
}
return Container(
child: Text(("CHILD")),
);
}
}
Widget _buildWaitingScreen() {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: CircularProgressIndicator(),
),
);
}
the most important stuff of the login page::
var user = Model.of(context).user;
user = await getUserByIdClient()
if (user.loginError == false){
print (user);
widget.onSignedIn();
}
Here's my inherited widget:
class Model extends InheritedWidget {
Model({Key key, Widget child, this.user}) : super(key: key, child: child);
final User user;
#override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
static Model of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(Model) as Model);
}
}
From what I understand, it seems that you're looking in to storing user session in your app. One way of doing this is by storing user credentials in shared_preferences (i.e. userId). Depending on your use case, your backend might require users to re-authenticate, so keep an eye on that.
Store user credentials after login.
// Obtain shared preferences.
final prefs = await SharedPreferences.getInstance();
// Save user details on userId
String userId = ...;
await prefs.setString('userId', userId);
When has been signed out, you can remove the data.
await prefs.remove('userId');
For verifying user session, you can then check the stored value. If it's empty, logout the user.
final String? userId = prefs.getString('userId');
if(userId != null){
// User is logged-in
} else {
// User is signed-out
}
In Angular, one can use the canActivate for route guarding.
In Flutter, how would one go about it? Where is the guard placed? How do you guard routes?
I'm thinking along the lines of this:
User logged in. Their token is stored in Shared Preference (the right way to store tokens? )
User closed the app.
User opens app again. As the application starts, it determines if user is logged in (perhaps a service that checks the storage for token), and then
If logged in, load the homepage route
If not logged in, load the login page
I was stumbling over this problem too and ended up using a FutureBuilder for this. Have a look at my routes:
final routes = {
'/': (BuildContext context) => FutureBuilder<AuthState>(
// This is my async call to sharedPrefs
future: AuthProvider.of(context).authState$.skipWhile((_) => _ == null).first,
builder: (BuildContext context, AsyncSnapshot<AuthState> snapshot) {
switch(snapshot.connectionState) {
case ConnectionState.done:
// When the future is done I show either the LoginScreen
// or the requested Screen depending on AuthState
return snapshot.data == AuthState.SIGNED_IN ? JobsScreen() : LoginScreen()
default:
// I return an empty Container as long as the Future is not resolved
return Container();
}
},
),
};
If you want to reuse the code across multiple routes you could extend the FutureBuilder.
I don't think there is a route guarding mechanism per se, but you can do logic in the main function before loading the app, or use the onGenerateRoute property of a MaterialApp. One way to do that in your case is to await an asynchronous function that checks if the user is logged in before loading the initial route. Something like
main() {
fetchUser().then((user) {
if (user != null) runApp(MyApp(page: 'home'));
else runApp(MyApp(page: 'login'));
});
}
But you may also be interested in the way the Shrine app does it. They have the login page as the initial route in any case and remove it if the user is logged in. That way the user sees the login page until it has been determined whether or not they log in. I've included the relevant snippet below.
class ShrineApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shrine',
home: HomePage(),
initialRoute: '/login',
onGenerateRoute: _getRoute,
);
}
Route<dynamic> _getRoute(RouteSettings settings) {
if (settings.name != '/login') {
return null;
}
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => LoginPage(),
fullscreenDialog: true,
);
}
}
If you don't want them to see the login page at all if they are logged in, use the first approach and you can control the splash screen that shows before runApp has a UI by exploring this answer.
You can use auto_route extension. This extension is awesome for dealing with routes and provides good way of using guards. Please refer to the documentation:
https://pub.dev/packages/auto_route#route-guards
I came up with the following solution for a web project, which allows me to easily introduce guarded routes without having to worry that an unauthorized user is able to access sensitive information.
The GuardedRoute class looks like this:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:kandabis_core/core.dart' as core;
Widget _defaultTransitionsBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return child;
}
class GuardedRoute extends PageRouteBuilder {
GuardedRoute({
#required final String guardedRoute,
#required final String fallbackRoute,
#required final Stream<bool> guard,
#required final core.Router router,
final RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
final bool maintainState = true,
final Widget placeholderPage,
})
: super(
transitionsBuilder: transitionsBuilder,
maintainState: maintainState,
pageBuilder: (context, animation, secondaryAnimation) =>
StreamBuilder(
stream: guard,
builder: (context, snapshot) {
if (snapshot.hasData) {
// navigate to guarded route
if (snapshot.data == true) {
return router.routes[guardedRoute](context);
}
// navigate to fallback route
return router.routes[fallbackRoute](context);
}
// show a placeholder widget while the guard stream has no data yet
return placeholderPage ?? Container();
}
),
);
}
Using the guarded route is easy. You can define a guarded route and a fallback route (like a login page). Guard is a Stream which decides if the user can navigate to the guarded route. this is my Router class which shows how to use the GuardedRoute class:
class BackendRouter extends core.BackendRouter {
BackendRouter(
this._authenticationProvider,
this._logger
);
static const _tag = "BackendRouter";
core.Lazy<GlobalKey<NavigatorState>> _navigatorKey =
core.Lazy(() => GlobalKey<NavigatorState>());
final core.AuthenticationProvider _authenticationProvider;
final core.Logger _logger;
#override
Map<String, WidgetBuilder> get routes => {
core.BackendRoutes.main: (context) => MainPage(),
core.BackendRoutes.login: (context) => LoginPage(),
core.BackendRoutes.import: (context) => ImportPage(),
};
#override
Route onGenerateRoute(RouteSettings settings) {
if (settings.name == core.BackendRoutes.login) {
return MaterialPageRoute(
settings: settings,
builder: routes[settings.name]
);
}
return _guardedRoute(settings.name);
}
#override
GlobalKey<NavigatorState> get navigatorKey => _navigatorKey();
#override
void navigateToLogin() {
_logger.i(_tag, "navigateToLogin()");
navigatorKey
.currentState
?.pushNamed(core.BackendRoutes.login);
}
#override
void navigateToImporter() {
_logger.i(_tag, "navigateToImporter()");
navigatorKey
.currentState
?.pushReplacement(_guardedRoute(core.BackendRoutes.import));
}
GuardedRoute _guardedRoute(
String route,
{
maintainState = true,
fallbackRoute = core.BackendRoutes.login,
}) =>
GuardedRoute(
guardedRoute: route,
fallbackRoute: fallbackRoute,
guard: _authenticationProvider.isLoggedIn(),
router: this,
maintainState: maintainState,
placeholderPage: SplashPage(),
);
}
And your application class looks like this:
class BackendApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
// get router via dependency injection
final core.BackendRouter router = di.get<core.BackendRouter>();
// create app
return MaterialApp(
onGenerateRoute: (settings) => router.onGenerateRoute(settings),
navigatorKey: router.navigatorKey,
);
}
}
You can use flutter_modular package to do that. It´s a library that try to keep the same features than angular. Take a look of it documentation.
Modular Docs
My solution is to build in a route guard system, much like the other libraries out there but where we can still use the original Navigator where needed, open a modal as a named route, chain guards, and add redirects. Its really basic but can be built on quite easily.
It seems like a lot but you'll just need 3 new files to maintain along with your new guards:
- router/guarded_material_page_route.dart
- router/route_guard.dart
- router/safe_navigator.dart
// Your guards go in here
- guards/auth_guard.dart
...
First create a new class that extends MaterialPageRoute, or MaterialWithModalsPageRoute if you're like me and want to open the Modal Bottom Sheet package. I've called mine GuardedMaterialPageRoute
class GuardedMaterialPageRoute extends MaterialWithModalsPageRoute {
final List<RouteGuard> routeGuards;
GuardedMaterialPageRoute({
// ScrollController is only needed if you're using the modals, as i am in this example.
#required Widget Function(BuildContext, [ScrollController]) builder,
RouteSettings settings,
this.routeGuards = const [],
}) : super(
builder: builder,
settings: settings,
);
}
Your route guards will look like this:
class RouteGuard {
final Future<bool> Function(BuildContext, Object) guard;
RouteGuard(this.guard);
Future<bool> canActivate(BuildContext context, Object arguments) async {
return guard(context, arguments);
}
}
You can now add GuardedMaterialPageRoutes to your router file like so:
class Routes {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case homeRoute:
// These will still work with our new Navigator!
return MaterialPageRoute(
builder: (context) => HomeScreen(),
settings: RouteSettings(name: homeRoute),
);
case locationRoute:
// Following the same syntax, just with a routeGuards array now.
return GuardedMaterialPageRoute(
// Again, scrollController is only if you're opening a modal as a named route.
builder: (context, [scrollController]) {
final propertiesBloc = BlocProvider.of<PropertiesBloc>(context);
final String locationId = settings.arguments;
return BlocProvider(
create: (_) => LocationBloc(
locationId: locationId,
propertiesBloc: propertiesBloc,
),
child: LocationScreen(),
);
},
settings: RouteSettings(name: locationRoute),
routeGuards: [
// Now inject your guards, see below for what they look like.
AuthGuard(),
]
);
...
Create your async guard classes like so, as used above in our router.
class AuthGuard extends RouteGuard {
AuthGuard() : super((context, arguments) async {
final auth = Provider.of<AuthService>(context, listen: false);
const isAnonymous = await auth.isAnonymous();
return !isAnonymous;
});
}
Now you'll need a new class that handles your navigation. Here you check if you have access and simply run through each guard:
class SafeNavigator extends InheritedWidget {
static final navigatorKey = GlobalKey<NavigatorState>();
#override
bool updateShouldNotify(SafeNavigator oldWidget) {
return false;
}
static Future<bool> popAndPushNamed(
String routeName, {
Object arguments,
bool asModalBottomSheet = false,
}) async {
Navigator.of(navigatorKey.currentContext).pop();
return pushNamed(routeName, arguments: arguments, asModalBottomSheet: asModalBottomSheet);
}
static Future<bool> pushNamed(String routeName, {
Object arguments,
bool asModalBottomSheet = false,
}) async {
// Fetch the Route Page object
final settings = RouteSettings(name: routeName, arguments: arguments);
final route = Routes.generateRoute(settings);
// Check if we can activate it
final canActivate = await _canActivateRoute(route);
if (canActivate) {
// Only needed if you're using named routes as modals, under the hood the plugin still uses the Navigator and can be popped etc.
if (asModalBottomSheet) {
showCupertinoModalBottomSheet(
context: navigatorKey.currentContext,
builder: (context, scrollController) =>
(route as GuardedMaterialPageRoute)
.builder(context, scrollController));
} else {
Navigator.of(navigatorKey.currentContext).push(route);
}
}
return canActivate;
}
static Future<bool> _canActivateRoute(MaterialPageRoute route) async {
// Check if it is a Guarded route
if (route is GuardedMaterialPageRoute) {
// Check all guards on the route
for (int i = 0; i < route.routeGuards.length; i++) {
// Run the guard
final canActivate = await route.routeGuards[i]
.canActivate(navigatorKey.currentContext, route.settings.arguments);
if (!canActivate) {
return false;
}
}
}
return true;
}
}
To make it all work you will need to add the SafeNavigator key to your Material app:
MaterialApp(
navigatorKey: SafeNavigator.navigatorKey,
...
)
And now you can navigate to your routes and check if you have access to them like this:
// Opens a named route, either Guarded or not.
SafeNavigator.pushNamed(shortlistRoute);
// Opens a named route as a modal
SafeNavigator.pushNamed(shortlistRoute, asModalBottomSheet: true);
// Pops the current route and opens a named route as a modal
SafeNavigator.popAndPushNamed(shortlistRoute, asModalBottomSheet: true);