flutter
/

GetX Authentication Flow: Login, Token Storage & Route Guards

Last Sync: Today

On this page

11
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Authentication Flow: Login, Token Storage & Route Guards

Introduction

Authentication is a core part of many apps. GetX provides a clean, reactive way to manage user state, store tokens, and protect routes. With its built‑in dependency injection, reactive state, and middleware, you can build a robust authentication flow with minimal boilerplate. This guide covers everything from login to token persistence and route guards.

  1. Setting Up the AuthController

Create a controller that holds the user's authentication state. Use reactive variables to reflect login status and user data. The controller will also handle login, logout, and token storage.

DARTRead-only
1
class AuthController extends GetxController {
  final storage = GetStorage();
  var isLoggedIn = false.obs;
  var user = User().obs;

  @override
  void onInit() {
    super.onInit();
    // Check for existing token on app start
    String? token = storage.read('token');
    if (token != null) {
      isLoggedIn.value = true;
      // Optionally fetch user data
      fetchUser();
    }
  }

  Future<void> login(String email, String password) async {
    try {
      // Call your API
      final response = await api.login(email, password);
      // Save token
      storage.write('token', response.token);
      user.value = response.user;
      isLoggedIn.value = true;
    } catch (e) {
      Get.snackbar('Error', e.toString());
    }
  }

  void logout() {
    storage.remove('token');
    isLoggedIn.value = false;
    user.value = User();
    Get.offAllNamed('/login');
  }
}

  1. Token Persistence with GetStorage

Use GetStorage to save the authentication token. Initialize GetStorage in main() and then read/write tokens inside the controller. The token can be used in API calls by attaching it to headers.

DARTRead-only
1
// In main.dart
void main() async {
  await GetStorage.init();
  runApp(MyApp());
}

// In AuthController
String? get token => storage.read('token');

// In API service
class ApiService {
  Future<LoginResponse> login(String email, String password) async {
    final response = await http.post(
      Uri.parse('$baseUrl/login'),
      body: {'email': email, 'password': password},
    );
    return LoginResponse.fromJson(json.decode(response.body));
  }

  Future<User> fetchUser() async {
    final token = Get.find<AuthController>().token;
    final response = await http.get(
      Uri.parse('$baseUrl/me'),
      headers: {'Authorization': 'Bearer $token'},
    );
    return User.fromJson(json.decode(response.body));
  }
}

  1. Protecting Routes with Middleware

Use GetMiddleware to guard routes that require authentication. Override redirect to check login status and redirect to login if not authenticated.

DARTRead-only
1
class AuthMiddleware extends GetMiddleware {
  @override
  RouteSettings? redirect(String? route) {
    final auth = Get.find<AuthController>();
    if (!auth.isLoggedIn.value) {
      Get.snackbar('Access Denied', 'Please login first');
      return RouteSettings(name: '/login');
    }
    return null;
  }
}

// In your routes
GetPage(
  name: '/profile',
  page: () => ProfilePage(),
  middlewares: [AuthMiddleware()],
);

  1. Login Page UI

Create a simple login page with reactive validation and loading state. Use GetX or Obx to react to the authentication state.

DARTRead-only
1
class LoginPage extends GetView<AuthController> {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(controller: emailController, decoration: InputDecoration(labelText: 'Email')),
            TextField(controller: passwordController, obscureText: true, decoration: InputDecoration(labelText: 'Password')),
            SizedBox(height: 20),
            Obx(() => ElevatedButton(
              onPressed: controller.isLoading.value
                  ? null
                  : () => controller.login(emailController.text, passwordController.text),
              child: controller.isLoading.value
                  ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
                  : Text('Login'),
            )),
          ],
        ),
      ),
    );
  }
}

  1. Redirecting After Login

After successful login, use Get.offAllNamed to navigate to the home screen and remove the login page from the stack.

DARTRead-only
1
// Inside AuthController login method
Get.offAllNamed('/home');

  1. Logout and Cleanup

Implement logout by clearing the token, resetting the reactive state, and navigating to the login screen.

DARTRead-only
1
void logout() {
  storage.remove('token');
  isLoggedIn.value = false;
  Get.offAllNamed('/login');
}

  1. Using AuthController Throughout the App

Since the controller is permanent (or registered globally), you can access it anywhere with Get.find<AuthController>(). For example, in a profile page to display the user's name.

DARTRead-only
1
class ProfilePage extends GetView<AuthController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: Center(
        child: Obx(() => Text('Hello, ${controller.user.value.name}')),
      ),
    );
  }
}

Best Practices

  • Make AuthController permanent – Use permanent: true or extend GetxService so it never gets disposed.
  • Use middleware for route protection – Centralise authentication checks.
  • Store tokens securely – For production, consider using FlutterSecureStorage for sensitive data.
  • Clear token on logout – Always remove the token and reset state.
  • Handle token expiration – In your API client, check for 401 responses and automatically redirect to login.

Common Mistakes

  • ❌ Not initializing GetStorage before use – Causes errors. ✅ Call await GetStorage.init() in main.
  • ❌ Making AuthController non‑permanent – It may be disposed when a route is popped. ✅ Set permanent: true or use GetxService.
  • ❌ Forgetting to redirect after login – User may stay on login page. ✅ Use Get.offAllNamed to replace the route.
  • ❌ Not handling token refresh – After logout, the token is gone, but the user may still have access to protected routes if not guarded properly. ✅ Use middleware to check state on every navigation.

Conclusion

GetX makes authentication flows straightforward. By combining reactive state, persistent storage with GetStorage, and route guards with middleware, you can build a secure and user‑friendly login system. The patterns shown here scale from simple apps to complex enterprise solutions.

Try it yourself

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';

void main() async {
  await GetStorage.init();
  runApp(MyApp());
}

class AuthController extends GetxController {
  final storage = GetStorage();
  var isLoggedIn = false.obs;
  var isLoading = false.obs;

  @override
  void onInit() {
    super.onInit();
    isLoggedIn.value = storage.read('token') != null;
  }

  Future<void> login(String email, String password) async {
    isLoading.value = true;
    await Future.delayed(Duration(seconds: 1)); // Simulate network call
    if (email == 'user@example.com' && password == 'pass') {
      storage.write('token', 'fake-token');
      isLoggedIn.value = true;
      Get.offAllNamed('/home');
    } else {
      Get.snackbar('Error', 'Invalid credentials');
    }
    isLoading.value = false;
  }

  void logout() {
    storage.remove('token');
    isLoggedIn.value = false;
    Get.offAllNamed('/login');
  }
}

class AuthMiddleware extends GetMiddleware {
  @override
  RouteSettings? redirect(String? route) {
    final auth = Get.find<AuthController>();
    if (!auth.isLoggedIn.value && route != '/login') {
      return RouteSettings(name: '/login');
    }
    return null;
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Get.put(AuthController(), permanent: true);
    return GetMaterialApp(
      initialRoute: '/',
      getPages: [
        GetPage(name: '/', page: () => SplashPage()),
        GetPage(name: '/login', page: () => LoginPage()),
        GetPage(name: '/home', page: () => HomePage(), middlewares: [AuthMiddleware()]),
        GetPage(name: '/profile', page: () => ProfilePage(), middlewares: [AuthMiddleware()]),
      ],
    );
  }
}

class SplashPage extends StatelessWidget {
  final auth = Get.find<AuthController>();

  @override
  Widget build(BuildContext context) {
    Future.delayed(Duration(seconds: 1), () {
      if (auth.isLoggedIn.value) {
        Get.offAllNamed('/home');
      } else {
        Get.offAllNamed('/login');
      }
    });
    return Scaffold(body: Center(child: CircularProgressIndicator()));
  }
}

class LoginPage extends StatelessWidget {
  final auth = Get.find<AuthController>();
  final emailCtrl = TextEditingController();
  final passCtrl = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(controller: emailCtrl, decoration: InputDecoration(labelText: 'Email (user@example.com)')),
            TextField(controller: passCtrl, obscureText: true, decoration: InputDecoration(labelText: 'Password (pass)')),
            SizedBox(height: 20),
            Obx(() => ElevatedButton(
              onPressed: auth.isLoading.value ? null : () => auth.login(emailCtrl.text, passCtrl.text),
              child: auth.isLoading.value
                  ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
                  : Text('Login'),
            )),
          ],
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  final auth = Get.find<AuthController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Welcome! You are logged in.'),
            ElevatedButton(
              onPressed: () => Get.toNamed('/profile'),
              child: Text('Profile'),
            ),
            ElevatedButton(
              onPressed: auth.logout,
              child: Text('Logout'),
            ),
          ],
        ),
      ),
    );
  }
}

class ProfilePage extends StatelessWidget {
  final auth = Get.find<AuthController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('This is a protected page.'),
            ElevatedButton(
              onPressed: () => Get.back(),
              child: Text('Back'),
            ),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

How do you make AuthController permanent so it never gets disposed?

A
Set permanent: true in Get.put
B
Extend GetxService
C
Both A and B
D
None of the above
Q2
of 3

Which GetX feature is used to protect routes by checking authentication status?

A
GetBuilder
B
GetMiddleware
C
GetPage
D
GetView
Q3
of 3

What method should you use to navigate to the home screen after login and remove the login page from the back stack?

A
Get.to
B
Get.off
C
Get.offAllNamed
D
Get.toAndRemoveUntil

Frequently Asked Questions

How do I persist user session after app restart?

Store the token in GetStorage and read it in onInit of AuthController. If token exists, set isLoggedIn = true and optionally fetch user data.

Can I use GetX middleware to redirect to login on 401?

Yes, you can listen to API responses and call Get.offAllNamed('/login') when a 401 is received.

How to protect routes that depend on roles?

In your middleware, after checking isLoggedIn, you can also check the user's role and redirect if insufficient.

Should I store the user object in GetStorage as well?

If the user data is small, you can. Otherwise, fetch it after login and store only the token.

How to show a loading indicator during login?

Use an isLoading reactive boolean and disable the button while true.

Previous

getx permanent controller

Next

getx pagination

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.