In most real-world apps, users can have different roles and permissions that grant or deny them access to certain parts of the app.
To manage these accesses, we can use a very common concept called “route guards”.
In this tutorial, we are going to create a super simple app, with only two screens. One of them will be public, while the other will be restricted to premium members. If I try to navigate to the premium area, I will be automatically redirected to the public screen.
In order to do so in a simple manner, we will take advantage the navigation features of the GetX package.
So without a further ado, let’s go!
Table of Contents
- 1. Setup the Flutter project
- 2. Create the screens
- 3. Setup the navigation
- 4. Add some content to our pages
- 5. Setup a fake Authentication Service
- 6. Create a middleware to manage route access
- 7. Test the guard
- 8. Going Further
1. Setup the Flutter project
If you are reading this tutorial, I assume you have basic knowledge about flutter, so I won’t go into much details in this part.
Let’s start by creating a new flutter project, open a terminal an enter the following command:
flutter create getx_route_guard
Next, we will need to install the GetX package:
cd getx_route_guard && flutter pub add get
This is the only dependency we will need for this tutorial!
If you are not familiar with the GetX package, it is a very helpful tool to help you build flutter apps faster and with less boilerplate. It has a lot of very cool features, but the three main pillars are the state management, navigation and dependency injection.
We will use only the navigation feature in this tutorial, but feel free to read more about this very cool package here.
Alright, now we are all set, so open the project in Android Studio or your favorite IDE, and let’s start to write some code!
2. Create the screens
First of all, let’s create the two screens we will need.
In a new folder home
, create a file named home_view.dart
with a simple empty StatelessWidget:
// lib/home/home.dart
import 'package:flutter/material.dart';
class HomeView extends StatelessWidget {
const HomeView({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Container();
}
}
And in a premium_area
folder, create the premium_area_view.dart
file:
// lib/premium_area/premium_area
import 'package:flutter/material.dart';
class PremiumAreaView extends StatelessWidget {
const PremiumAreaView({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Container();
}
}
Then, remove everything in /lib/main.dart
and replace it by this:
// lib/main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return GetMaterialApp(
title: "My Awesome App",
);
}
}
The only thing we changed here is that we now use GetMaterialApp instead of MaterialApp. This allows us to use the GetX routing functionalities and many other GetX features.
Of course if you try to compile now, you will get an error. That’s because our app doesn’t return any screen. We need to define routes to navigate to the 2 pages we just created.
3. Setup the navigation
Let’s first create a new file in the root of our project called navigation.dart
which will contain our routes:
// lib/navigation.dart
abstract class Routes {
static const home = '/home';
static const premiumArea = '/premium-area';
}
That way, it will be easier to manage our routes, and they will be easily accessible in our app, simply using Routes.home
for example.
Now, we need to actually attach our views to routes. For that, GetX has a special class named GetPage that we can use like so:
GetPage(
name: '/some-route-path', // The path of the route
page: () => MyPageView(), // The view corresponding to this path
),
This is pretty self explanatory, we create a GetPage instance with a path and the view to be redirected when accessing this path.
In our case, we only have two pages, so let’s create an array just after the Routes class:
// lib/navigation.dart
final appPages = [
GetPage(
name: Routes.home,
page: () => HomeView(),
),
GetPage(
name: Routes.premiumArea,
page: () => const PremiumAreaView(),
),
];
Then, we simply need to pass this array to the GetMaterialApp, so that our routes become available in our app:
// lib/main.dart
Widget build(BuildContext context) {
return GetMaterialApp(
title: "My App",
getPages: appPages, // I add my list of pages here
initialRoute: Routes.home, // This is the page that should be rendered on app launch
);
}
And that’s it for the routing!
Now, run the app again, you should see this message in your terminal:
Which means our routing works well. However, you might have noticed a little problem, if you check your emulator, you should see… A black screen !
The routing works properly, but there is nothing to render in our views yet. So let’s do it.
4. Add some content to our pages
For now, our pages are just empty widgets.
In the home_view.dart
file, add the following content:
// lib/home/home_view.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_auth_guard/navigation.dart';
class HomeView extends StatelessWidget {
HomeView({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome to my awesome App!',
style: context.textTheme
.headline5, // context is made available by the get package
),
const SizedBox(
height: 16.0,
),
ElevatedButton(
onPressed: () {
Get.offAndToNamed(Routes.premiumArea);
},
child: const Text('GO TO PREMIUM AREA'),
),
],
),
),
),
);
}
}
It’s pretty basic flutter code, so I won’t go into details. This is a simple page with an AppBar, and a button in the middle to navigate to the premium area.
Just two things to note here. First, you might wonder how we access the context like this. This is made possible by the GetX package, and is very convenient to access some variables, like your theme.
Secondly, here we have an example of how to navigate to a route with GetX:
Get.offAndToNamed(YOUR_ROUTE_NAME);
This will remove the current page from the navigation stack and push the new one on top.
There are of course other ways to navigate, you can check the GetX documentation to learn more about it.
Alright, we are done with the home view, now let’s create our premium area:
// lib/premium_area/premium_area_view.dart
class PremiumAreaView extends StatelessWidget {
const PremiumAreaView({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Premium Area'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'This is a restricted area!',
style: context.textTheme
.headline5, // context is made available by the get package
),
const SizedBox(
height: 16.0,
),
ElevatedButton(
onPressed: () {
Get.offAndToNamed(Routes.home);
},
child: const Text('GO BACK TO HOME'),
),
],
),
),
),
);
}
}
Again, not much to say here, a very simple page, with a button to go back to our home page.
Now, restart your app, and check that the navigation is working properly. You should start on the home page, then if you click on the button, you should be redirected to the premium area, and then be able to navigate back to the home page.
So far so good, we have a very simple working app. Now, we still need to implement the most important feature: I want to prevent non premium users to access the premium area.
This is where the route guard comes in handy.
5. Setup a fake Authentication Service
Before creating the actual route guard, we will add the logic to handle premium members.
To keep this tutorial simple, we won’t create a real authentication flow. Instead, we will pretend that the user is already logged in. Then we can simply have a service that tells us if this user is authorized or not.
A good place for handling such logic is in a GetXService. Indeed, a GetXService is a class that is never removed from memory throughout the lifetime of the app, thus allowing us to access it anytime and anywhere with the Get.find()
function. This makes it the perfect place to store persistent data, such authentication information.
Let’s create a new services
folder, and inside a auth_service.dart
file:
// lib/services/auth_service.dart
import 'package:get/get.dart';
class AuthService extends GetxService {
Future<AuthService> init() async => this;
}
Now, let’s add a property to know if the current user is premium. This property should be able to change, if you logout and login with a different user for example. That is why we are going to use a reactive variable like so:
final RxBool isPremium = false.obs;
You might be wondering what is the RxBool type. This means that this variable is reactive, we can listen to its changes and do something in reaction to these changes.
There are few ways to make a variable reactive with GetX, you can find out more in the documentation. Here we just add .obs
at the end.
Next, let’s add a function to update the value of our variable:
void setIsPremium(bool newValue) {
isPremium.value = newValue;
}
The only thing to notice here is that you update a reactive variable value by updating its .value
property.
That’s pretty much it for our service, this is our complete file:
// lib/services/auth_service.dart
import 'package:get/get.dart';
class AuthService extends GetxService {
Future<AuthService> init() async => this;
final RxBool isPremium = false.obs;
void setIsPremium(bool newValue) {
isPremium.value = newValue;
}
}
One last thing we need to do, is to initialize our service when our app starts.
This can be done in our main.dart file, just before the runApp()
, so that our service will be available immediately:
// lib/main.dart
void main() async {
await Get.putAsync(() => AuthService().init());
runApp(const MyApp());
}
Get.putAsync
uses the dependency injection feature of GetX. I won’t go into details here, but basically, it initialize our service into the Get instance, so that it can be retrieved anywhere in our app with the Get.find()
function.
If you run the App, you will now this message in the terminal:
So now we are ready to create our route guard.
6. Create a middleware to manage route access
With GetX Navigation, you can attach middlewares to routes.
Middlewares are a way to execute some function when you navigate to a route. It can be anything you want: preload some data, or in our case, check that a user has the permission to access a page.
It is as simple as passing a list of GetMiddleware
instances to a page.
GetPage(
name: 'my-route-path',
page: () => const MyPageView(),
middlewares: [
// My middlewares here
MyMiddleware1(priority: 3),
MyMiddleware2(priority: 1),
MyMiddleware3(priority: 2),
]
)
You can give as many middlewares as you want to a route. Be mindful of the fact that they will be executed in the order you provide them. But you can also pass a priority parameter so that when you try to navigate to the page, each middleware will be called according to their priority.
Now, let’s create an actual middleware, create a new file called premium_guard.dart
in a middlewares
folder:
// lib/middlewares/premium_guard.dart
class PremiumGuard extends GetMiddleware {
// Our middleware content
}
We simply create our middleware class that extends GetMiddleware
. This class has callbacks for differents lifecycle hooks of the navigation to a page.
For example, you can override functions just before the page widget is being created, or when it is disposed.
In our case, we want to do some permission checking when a user tries to navigate to the page, and redirect them accordingly. For this, we can override the redirect
function, how convenient!
Let’s implement it:
// lib/middlewares/premium_guard.dart
class PremiumGuard extends GetMiddleware {
final authService = Get.find<AuthService>();
RouteSettings? redirect(String? route) {
return authService.isPremium.value
? null
: const RouteSettings(name: Routes.home);
}
}
Ok, what’s happening here?
First, we retrieve the instance of our AuthService
, as we will need it to check whether the user is premium or not.
Then, we override the redirect
function. In it, we check the isPremium
value in our AuthService
. If the user is premium, we return null, which means the navigation can continue normally. If we had other middlewares, the next one would be called.
However, if the user is not premium, we return a RouteSettings
object, with the name of the route we want the user to be redirect to, in our case the Home page.
We have the logic, now we just need to attach our middleware to a route. So let’s edit our navigation.dart
file to add our guard to the middlewares array:
// lib/navigation.dart
final appPages = [
GetPage(
name: Routes.home,
page: () => HomeView(),
),
GetPage(
name: Routes.premiumArea,
page: () => const PremiumAreaView(),
middlewares: [
PremiumGuard(), // Add the middleware here
],
),
];
Now, run your app again. If you try to navigate to the premium page, you should immediately be redirected to the home page!
You can change the value of isPremium
in our AuthService
to true, and verify that the navigation is now possible.
7. Test the guard
Changing the value of the isPremium
manually in the code is fine for testing, however it’s not going to help in a real-life app.
We are going to add a simple way to test our guard, by taking advantage of the reactive variable we created.
First, let’s add a switch button in our home_view.dart
to toggle the status of the current user:
// lib/home/home_view.dart
// ...
Text(
'Welcome to my awesome App!',
style: context.textTheme.headline5,
),
const SizedBox(
height: 16.0,
),
SwitchListTile(
title: const Text('isPremium value'),
value: _authService.isPremium.value,
onChanged: _authService.setIsPremium,
)
// ...
So we just created a switch button that call the setIsPremium
function in our AuthService
.
💡 In a real GetX app, it would be better to separate the logic in a page controller, but to keep this tutorial short, we will directly access the
AuthService
.
Now if you try to click on the switch button… Nothing happens! That’s because we have to remember that isPremium
is a reactive variable, but the HomeView is a stateless widget.
Luckily, with GetX, there is an easy way to listen to changes of a variable and redraw a widget. You simply need to wrap it in an Obx
widget, like so:
Obx(() {
return SwitchListTile(
title: const Text('isPremium value'),
value: _authService.isPremium.value,
onChanged: _authService.setIsPremium,
);
})
And now, if you tap on the switch button, it should update, and toggle the isPremium
value. You can then play around with the button and try to navigate to the premium area, to check that our guard works properly.
Going further
This is it for this tutorial! I hope it is now more clear to you how you can create middlewares to guard your routes.
Here we used it to restrict the access to some users, but you can of course do much more with middlewares, like doing backend calculations, checking route arguments, fetching data, etc…
Feel free to play around with the functions provided by the middleware. You could for example try to add a snackbar to notify the user that he tried to access an unauthorized page.
I hope this was helpful, and see you soon for another tutorial!