How to implement Route Guard in Flutter with GetX

Written on November 09, 2021

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.

navigation diagram

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!

💡 Get the code on github

Table of Contents

  1. 1. Setup the Flutter project
  2. 2. Create the screens
  3. 3. Setup the navigation
  4. 4. Add some content to our pages
  5. 5. Setup a fake Authentication Service
  6. 6. Create a middleware to manage route access
  7. 7. Test the guard
  8. 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:

terminal shows that the routing works

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:

Terminal shows that auth service has been created

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!


Do you have a project?

Build a website to showcase your brand, reinforce your online presence, create an app for your business...
We do it all! So let's get in touch!