Flutter Oauth2 Login Example

by Didin J. on Mar 05, 2024 Flutter Oauth2 Login Example

The comprehensive step-by-step Flutter tutorial on building secure mobile apps that login or authenticate to the OAuth2 server

In this Flutter tutorial, we will show you how to access secure REST API resources that are accessible by an authenticated user. To authenticate or log in, the Flutter application will log in to an OAuth2 authorization server and then use the authorized token to access the secure REST API endpoint. We will use all one and standalone resource and authorization servers using Node.js.

This tutorial is divided into several steps:

Let's get started with the main steps!


Step #1: Preparation

Install Flutter SDK

To install Flutter SDK, first, we have to download flutter_windows_3.3.1-stable.zip and then extract the file to your desired location.

mkdir C:\development
cd C:\development

Unzip flutter_windows_3.3.1-stable.zip to C:\development folder. Next, add flutter tools to the path by search edit the system environment variables. Click the environment variable then in the system variables edit path. Add this variable C:\development\flutter\bin.

Next, to make iOS and Android binaries that can be downloaded ahead of time, type this Flutter command.

flutter precache

Check the Required Dependencies

To check the environment and display a report to the cmd window to find dependencies that are required to install, type this command.

flutter doctor

We have this summary in the cmd.

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.3.1, on Microsoft Windows [Version 10.0.19044.1889], locale en-ID)
[!] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    X cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    X Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[√] Chrome - develop for the web
[X] Visual Studio - develop for Windows
    X Visual Studio not installed; this is necessary for Windows development.
      Download at https://visualstudio.microsoft.com/downloads/.
      Please install the "Desktop development with C++" workload, including all of its default components
[√] Android Studio (version 2021.2)
[√] IntelliJ IDEA Community Edition (version 2022.1)
[√] VS Code (version 1.71.0)
[√] Connected device (3 available)
[√] HTTP Host Availability

! Doctor found issues in 2 categories.

To solve those problems, first install SDKManager cmdline-tools.

sdkmanager --install "cmdline-tools;latest"

To accept SDK licenses, type this command.

flutter doctor --android-licenses

Type 'y' multiple times to accept all licenses. To install Visual Studio, go to https://visualstudio.microsoft.com/downloads/ then download the Visual Studio community edition. Install it then choose Desktop development with C++ in Visual Studio workload. After everything is done, check again by typing the flutter doctor command. You should see no issue in the result.

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.3.1, on Microsoft Windows [Version 10.0.19044.1889], locale en-ID)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop for Windows (Visual Studio Community 2022 17.3.3)
[√] Android Studio (version 2021.2)
[√] IntelliJ IDEA Community Edition (version 2022.1)
[√] VS Code (version 1.71.0)
[√] Connected device (3 available)
[√] HTTP Host Availability

• No issues found!

Setup IDE

We need to set up the IDE to make it work with the Flutter app easily and compatibly. Start the Android Studio (we will use this IDE) close all opened projects then choose Plugins.

Flutter Oauth2 Login Example - android studio

Type Flutter in the plugins marketplace then press Enter. Next, click the Install button on the Flutter. If there's a prompt to install Dart, click Yes. Restart the IDE when prompted to ask for a restart.

Now, the Flutter is ready to develop Android and iOS mobile apps.


Step #2: Create a Flutter Application

After the IDE setup is complete, it will back to the Android starting dialog.

Flutter Oauth2 Login Example - android studio welcome

Choose `New Flutter Project` then choose `Flutter`. Browse to the previously installed Flutter SDK directory then click the next button. Fill in all required fields like below.

Flutter Oauth2 Login Example - flutter new

Click the finish button then click the create button to create a new folder. After Android Studio loaded and installed all dependencies, run the Flutter application for the first time. In the Android Studio toolbar, choose the Chrome (web) and main.dart then click the play button.

Flutter Oauth2 Login Example - flutter run

The Chrome browser will open and show this Flutter application.

Flutter Oauth2 Login Example - flutter chrome


Step #3: Create Flutter API Interceptor

We need to use an access token in every secure request. For that, we will intercept all HTTP requests to include the Authorization header. Also, we will need secure storage to save the JWT token from successful login or get a token and an HTTP interceptor to intercept every secure request to the REST API. For that, install this package by opening and editing the pubspec.yaml then adds this dependency.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  http: ^0.13.6
  flutter_secure_storage: ^9.0.0
  http_interceptor: ^1.0.1

Next, click the "Packages get" button in the Flutter commands at the top of pubspec.yml content. That command will install the registered dependencies.

Flutter Oauth2 Login Example - put get

Next, create a new services folder and then create a new Dart file api_interceptor.dart. 

lib/services/api_interceptor.dart

Open that file then add these lines of Dart code.

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter_oauth2/services/auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http_interceptor/http/interceptor_contract.dart';
import 'package:http_interceptor/models/request_data.dart';
import 'package:http_interceptor/models/response_data.dart';

class ApiInterceptor implements InterceptorContract {
  final storage = const FlutterSecureStorage();
  final AuthService authService = AuthService();

  Future<String> get tokenOrEmpty async {
    var token = await storage.read(key: "token");
    if (token == null) return "";
    return token;
  }

  @override
  Future<RequestData> interceptRequest({required RequestData data}) async {
    String token = await tokenOrEmpty;
    try {
      data.headers["Authorization"] = "Bearer $token";
    } catch (e) {
      if (kDebugMode) {
        print(e);
      }
    }
    return data;
  }

  @override
  Future<ResponseData> interceptResponse({required ResponseData data}) async {
    var refreshToken = await storage.read(key: "refresh_token");
    if (data.statusCode == 401 && refreshToken != null) {
      var res = await authService.refreshToken(refreshToken);
      var data = jsonDecode(res!.body);
      await storage.write(key: "token", value: data['access_token']);
      await storage.write(key: "refresh_token", value: data['refresh_token']);
    }
    return data;
  }
}

As you see, there's not only a request but also there is a response interception which is used to intercept the 401 response status from the API request and then check if there's a refresh token exists. If a refresh token exists then do a POST request to get a new access token by refresh token. 


Step #4: Create Flutter HTTP Service

As we mentioned in the first paragraph, we will use the HTTP library package to access the RESTful API from the OAuth2 server. Before creating the services, we need to create a package and a Dart file lib/helpers/constant.dart that contains all required constants variables. Fill this file with a class that contains the constructor and a variable of the REST API URL.

class Constants {
  Constants._();
  static const baseUrl = 'http://192.168.1.13:3000';
}

That URL is an OAuth2 and Resource server that runs on this computer and uses a local network address. Of course, when we run Android or iOS apps should use the same local network.

Next, create a class or object lib/services/api_service.dart where we will put all CRUD (POST, GET, PUT, DELETE) methods to all endpoints of the OAuth2 and REST API. In that file, add a class of API service.

class ApiService {

}

Add the imports before the class.

import 'package:flutter_oauth2/helpers/constant.dart';
import 'package:flutter_oauth2/services/api_interceptor.dart';
import 'package:http/http.dart';
import 'package:http_interceptor/http/intercepted_client.dart';

As you can see in the imports, we will use a customized intercepted HTTP client instead of the default Flutter HTTP. Also, there is an API interceptor import that will created in the next step. Next, declare a Client object that implements the API interceptor inside the ApiService class.

  Client client = InterceptedClient.build(interceptors: [
    ApiInterceptor(),
  ]);

Next, add a request after the client object to get the secure text from the secure endpoint.

  Future<Response> getSecretArea() async {
    var secretUrl = Uri.parse('${Constants.baseUrl}/secret');
    final res = await client.get(secretUrl);
    return res;
  }


Step #5: Create Flutter Authentication Service

Create the authentication services inside the same package lib/services/auth.dart then fill that file with these Dart codes.

import 'dart:convert';

import 'package:flutter_oauth2/helpers/constant.dart';
import 'package:http/http.dart';

class AuthService {
  var loginUri = Uri.parse('${Constants.baseUrl}/oauth/token');
  var registerUri = Uri.parse('${Constants.baseUrl}/oauth/signup');

  Future<Response?> login(String username, String password) async {
    String client = 'express-client';
    String secret = 'express-secret';
    String basicAuth = 'Basic ${base64.encode(utf8.encode('$client:$secret'))}';
    var res = await post(
        loginUri,
        headers: <String, String>{'authorization': basicAuth},
        body: {
          "username": username,
          "grant_type": "password",
          "password": password
        }
    );
    return res;
  }

  Future<Response?> refreshToken(String token) async {
    String client = 'express-client';
    String secret = 'express-secret';
    String basicAuth = 'Basic ${base64.encode(utf8.encode('$client:$secret'))}';
    var res = await post(
        loginUri,
        headers: <String, String>{'authorization': basicAuth},
        body: {
          "refresh_token": token,
          "grant_type": "refresh_token"
        }
    );
    return res;
  }

  Future<Response?> register(String username, String password, String name) async {
    var res = await post(
        registerUri,
        body: {
          "username": username,
          "password": password,
          "name": name
        }
    );
    return res;
  }
}

As you see, there are requests to log in or get a token, get a refresh token, and register a new user. We must include the basic authorization that has client and secret which is registered in the OAuth2 server. The difference between a get token and a refresh token is a grant-type field in the body request.


Step #6: Create Login and Register Screen

To build the views or screens for the mobile apps in the Flutter application, we need to prepare the required dependencies, libraries, or components. This tutorial required an email validator to validate an email text field, flutter easy loading to display a nice progress indicator, and custom fonts. Add dependencies first to the pubspec.yaml file.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  http: ^0.13.6
  flutter_secure_storage: ^9.0.0
  http_interceptor: ^1.0.1
  email_validator: ^2.1.17
  flutter_easyloading: ^3.0.5
  intl: ^0.19.0

Don't forget to click the Pub Get button to download all dependencies. Next, create a fonts folder at the root of the project folder. Download the custom fonts (ours: Roboto https://fonts.google.com/specimen/Roboto) then extract and copy them to the newly created fonts folder. Register those fonts to the pubspec.yaml file.

  fonts:
    - family: Roboto
      fonts:
        - asset: fonts/Roboto-Black.ttf
        - asset: fonts/Roboto-BlackItalic.ttf
        - asset: fonts/Roboto-Bold.ttf
        - asset: fonts/Roboto-BoldItalic.ttf
        - asset: fonts/Roboto-Italic.ttf
        - asset: fonts/Roboto-Light.ttf
        - asset: fonts/Roboto-LightItalic.ttf
        - asset: fonts/Roboto-Medium.ttf
        - asset: fonts/Roboto-MediumItalic.ttf
        - asset: fonts/Roboto-Regular.ttf
        - asset: fonts/Roboto-Thin.ttf
        - asset: fonts/Roboto-ThinItalic.ttf

Next, we will configure the easy loading dependencies open end edit lib/main.dart and then add this import.

import 'package:flutter_easyloading/flutter_easyloading.dart';

Add a method after the main method for easy loading configuration.

void configLoading() {
  EasyLoading.instance
    ..displayDuration = const Duration(milliseconds: 2000)
    ..indicatorType = EasyLoadingIndicatorType.wave
    ..loadingStyle = EasyLoadingStyle.custom
    ..indicatorSize = 45.0
    ..radius = 10.0
    ..progressColor = const Color.fromARGB(255, 255, 200, 0)
    ..backgroundColor = const Color.fromARGB(255, 0, 0, 0)
    ..indicatorColor = const Color.fromARGB(255, 255, 200, 0)
    ..textColor = const Color.fromARGB(255, 255, 200, 0)
    ..maskColor = Colors.blue.withOpacity(0.5)
    ..userInteractions = true
    ..dismissOnTap = false;
}

Add that method calls inside the main methods.

void main() {
  runApp(const MyApp());
  configLoading();
}

Next, modify the default MyApp class to meet our requirements which are to set the initial route and register the router.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Auth Role',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      initialRoute: '/Login',
      routes: {
        '/Login': (context) => const LoginScreen(errMsg: '',),
        // '/Register': (context) => RegisterScreen(),
        // '/Home': (context) => HomeScreen(),
      },
      builder: EasyLoading.init(),
    );
  }
}

Before creating any screen, we need to add a slider right route class in lib/helpers/sliderightroute.dart to make animated slide navigation that will be used on each screen. Add these Dart codes to that file.

import 'package:flutter/cupertino.dart';

class SlideRightRoute extends PageRouteBuilder {
  final Widget page;

  SlideRightRoute({required this.page})
      : super(
    pageBuilder: (
        BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation,
        ) =>
    page,
    transitionsBuilder: (
        BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation,
        Widget child,
        ) =>
        SlideTransition(
          position: Tween<Offset>(
            begin: const Offset(-1, 0),
            end: Offset.zero,
          ).animate(animation),
          child: child,
        ),
  );
}

Next, create the package inside the lib folder lib/screens then add these files (login.dart, register.dart). Open and edit lib/screens/login.dart then add these Dart codes.

import 'dart:convert';

import 'package:email_validator/email_validator.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_oauth2/helpers/sliderightroute.dart';
import 'package:flutter_oauth2/screens/home.dart';
import 'package:flutter_oauth2/screens/register.dart';
import 'package:flutter_oauth2/services/auth.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class LoginScreen extends StatelessWidget {
  const LoginScreen({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;
  static const String _title = 'Login';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulLoginWidget(errMsg: errMsg,),
    );
  }
}

class StatefulLoginWidget extends StatefulWidget {
  const StatefulLoginWidget({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;

  @override
  // ignore: no_logic_in_create_state
  State<StatefulLoginWidget> createState() => _StatefulLoginWidget(errMsg: errMsg);
}

class _StatefulLoginWidget extends State<StatefulLoginWidget> {
  _StatefulLoginWidget({required this.errMsg});
  final String errMsg;
  final AuthService authService = AuthService();
  final storage = const FlutterSecureStorage();
  final _loginFormKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool isLoading = false;

  void checkToken() async {
    var token = await storage.read(key: "token");
    if (token != null) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        Navigator.pushReplacement(
            context, SlideRightRoute(page: const HomeScreen()));
      });
    }
  }

  @override
  void initState() {
    super.initState();
    checkToken();

    if (errMsg.isNotEmpty) {
      Future.delayed(Duration.zero, () {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(errMsg),
        ));
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      body: SingleChildScrollView(
        child: Form(
          key: _loginFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please login to enter the app!',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _emailController,
                  validator: (value) {
                    if(value==null) {
                      return 'Please enter your email';
                    } else {
                      return EmailValidator.validate(value) ? null: 'Please fill with the valid email';
                    }
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Email',
                    hintText: 'Email',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.email,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _passwordController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your password';
                    } else {
                      return null;
                    }
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  obscureText: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Password',
                    hintText: 'Password',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.password,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 235, 235, 235), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.login,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(color: Color.fromARGB(255, 128, 255, 0), width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if(_loginFormKey.currentState==null) {
                        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                          content: Text("Wrong email and password!"),
                        ));
                      } else {
                        if (_loginFormKey.currentState!.validate()) {
                          _loginFormKey.currentState!.save();
                          EasyLoading.show();
                          var res = await authService.login(
                              _emailController.text, _passwordController.text);

                          switch (res!.statusCode) {
                            case 200:
                              EasyLoading.dismiss();
                              var data = jsonDecode(res.body);
                              await storage.write(key: "token", value: data['access_token']);
                              await storage.write(key: "refresh_token", value: data['refresh_token']);
                              if (!context.mounted) return;
                              WidgetsBinding.instance.addPostFrameCallback((_) {
                                Navigator.pushReplacement(
                                    context, SlideRightRoute(page: const HomeScreen()));
                              });
                              break;
                            case 401:
                              EasyLoading.dismiss();
                              if (!context.mounted) return;
                              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                                content: Text("Wrong email or password!"),
                              ));
                              break;
                            default:
                              EasyLoading.dismiss();
                              if (!context.mounted) return;
                              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                                content: Text("Wrong email or password!"),
                              ));
                              break;
                          }
                        }
                      }
                    },
                    label: const Text('LOGIN',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(30),
                child: RichText(
                  text: TextSpan(
                    text: 'Not registered? ',
                    style: const TextStyle(
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w300,
                      color: Color.fromARGB(255, 0, 0, 0),
                    ),
                    children: <TextSpan>[
                      const TextSpan(
                          text: 'Register ',
                          style: TextStyle(
                            fontSize: 18.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w300,
                            color: Color.fromARGB(255, 0, 0, 0),
                          )),
                      TextSpan(
                          text: 'here ',
                          recognizer: TapGestureRecognizer()
                            ..onTap = () {
                              Navigator.push(context,
                                  SlideRightRoute(page: const RegisterScreen()));
                            },
                          style: const TextStyle(
                            fontSize: 18.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w700,
                            color: Color.fromARGB(255, 128, 255, 0),
                          )),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

As you can see, this screen uses StatelessWidget which includes the StatelfulWidget as the MaterialApp home. The StatefulWidget build returns the Scaffold that has a body the SingleChildScrollView to make the screen scrollable when a soft keyboard shows up or manually slides to scroll the screen. The Form is used as the SingleChildScrollView child and it has one Column that contains Text, TextFormField, and Button which are aligned stacked vertically.

The Button action (onPressed event) will validate the Form first then call the login method from the AuthService with the parameters email and password that get from the controllers. The response from the REST API might be 200 which means success to POST the login then redirecting to the HomeScreen that will be created in the next step. The other response codes will display the Snack Bar messages.

Next, open and edit `lib/screens/register.dart` and then add these lines of Dart codes. 

import 'dart:convert';

import 'package:email_validator/email_validator.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_oauth2/helpers/sliderightroute.dart';
import 'package:flutter_oauth2/screens/login.dart';
import 'package:flutter_oauth2/services/auth.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class RegisterScreen extends StatelessWidget {
  const RegisterScreen({Key? key}) : super(key: key);
  static const String _title = 'Register';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: StatefulRegisterWidget(),
    );
  }
}

class StatefulRegisterWidget extends StatefulWidget {
  const StatefulRegisterWidget({Key? key}) : super(key: key);

  @override
  State<StatefulRegisterWidget> createState() => _StatefulRegisterWidget();
}

class _StatefulRegisterWidget extends State<StatefulRegisterWidget> {
  final AuthService authService = AuthService();
  final storage = const FlutterSecureStorage();
  final _registerFormKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _nameController = TextEditingController();
  final _phoneController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      body: SingleChildScrollView(
        child: Form(
          key: _registerFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please fill your email, password, name, and phone',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _emailController,
                  validator: (value) {
                    if(value==null) {
                      return 'Please enter your email';
                    } else {
                      return EmailValidator.validate(value) ? null: 'Please fill with the valid email';
                    }
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Email',
                    hintText: 'Email',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.email,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _passwordController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your password';
                    } else if(value.length < 6) {
                      return 'Minimum 6 characters';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  obscureText: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Kata Sandi',
                    hintText: 'Kata Sandi',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.password,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _nameController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Name',
                    hintText: 'Name',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.perm_identity,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.update,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(color: Color.fromARGB(255, 128, 255, 0), width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if (_registerFormKey.currentState!.validate()) {
                        _registerFormKey.currentState!.save();
                        EasyLoading.show();
                        var res = await authService.register(
                            _emailController.text, _passwordController.text, _nameController.text);

                        switch (res!.statusCode) {
                          case 200:
                            EasyLoading.dismiss();
                            Navigator.push(
                                context, SlideRightRoute(page: const LoginScreen(errMsg: 'Registered Successfully',)));
                            break;
                          case 400:
                            EasyLoading.dismiss();
                            var data = jsonDecode(res.body);
                            if (data["msg"]) {
                              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                                content: Text(data["msg"].toString()),
                              ));
                            }
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Registration Failed"),
                            ));
                            break;
                          default:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Registration Failed"),
                            ));
                            break;
                        }
                      }
                    },
                    label: const Text('REGISTER',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(30),
                child: RichText(
                  text: TextSpan(
                    text: 'Already registered? ',
                    style: const TextStyle(
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w300,
                      color: Color.fromARGB(255, 0, 0, 0),
                    ),
                    children: <TextSpan>[
                      const TextSpan(
                          text: 'Login ',
                          style: TextStyle(
                            fontSize: 18.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w300,
                            color: Color.fromARGB(255, 235, 235, 235),
                          )),
                      TextSpan(
                          text: 'here ',
                          recognizer: TapGestureRecognizer()
                            ..onTap = () {
                              Navigator.push(context,
                                  SlideRightRoute(page: const LoginScreen(errMsg: '',)));
                            },
                          style: const TextStyle(
                            fontSize: 18.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w700,
                            color: Color.fromARGB(255, 128, 255, 0),
                          )),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

The structure of the Flutter widget same as the login screen. The Button action (onPressed event) will validate the Form first then call the register method from the AuthService with the parameters email, password, name, and phone that get from the controllers. The response from the REST API might be 201 which means success in POST the registered data then back to the Login screen. The other response codes will display the Snack Bar messages.


Step #7: Create a Secure Home Screen

We need to show a secure response from REST API and display it on the Home Screen. For that, create a Dart file inside the screens package lib/screens/home.dart. Open that file then add these lines of Dart codes.

import 'package:flutter/material.dart';
import 'package:flutter_oauth2/services/api_service.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';

import '../helpers/sliderightroute.dart';
import 'login.dart';

class HomeScreen extends StatelessWidget {
  static const String _title = 'Home';

  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: StatefulHomeWidget(),
    );
  }
}

class StatefulHomeWidget extends StatefulWidget {
  const StatefulHomeWidget({super.key});

  @override
  // ignore: no_logic_in_create_state
  State<StatefulHomeWidget> createState() => _StatefulHomeWidget();
}

class _StatefulHomeWidget extends State<StatefulHomeWidget> {
  late final String errMsg = "";
  late String secureMsg = "Not secured";
  final storage = const FlutterSecureStorage();
  final ApiService apiService = ApiService();

  @override
  void initState() {
    super.initState();

    if (errMsg.isNotEmpty) {
      Future.delayed(Duration.zero, () {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(errMsg),
        ));
      });
    }
  }

  Future getSecureData() async {
    Response resp = await apiService.getSecretArea();
    secureMsg = resp.body.toString();
    return resp;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Flutter OAuth2',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            tooltip: 'Sign Out',
            onPressed: () async {
              await storage.deleteAll();
              WidgetsBinding.instance.addPostFrameCallback((_) {
                Navigator.push(context,
                    SlideRightRoute(
                        page: const LoginScreen(errMsg: 'User logged out',)));
              });
            },
          ),
        ],
      ),
      body: Center(
        child: FutureBuilder(
          future: getSecureData(),
          builder: (context, snapshot) {
            return Text(
              secureMsg,
              textAlign: TextAlign.center,
              overflow: TextOverflow.visible,
              style: const TextStyle(
                height: 1.171875,
                fontSize: 18.0,
                fontFamily: 'Roboto',
                fontWeight: FontWeight.w500,
                color: Color.fromARGB(255, 0, 0, 0),
              ),
            );
          },
        )
      ),
    );
  }
}

There's only a secure API request, display the response in a string and a logout button on that screen.


Step #8: Run and Test Flutter Application

Before running the Flutter app, download the Nodejs OAuth2 server then run the PostgreSQL server. Extract the downloaded Node.js OAuth2 server, and go to the folder then run this server.

npm start

Run the iOS simulator or Android Simulator then choose the device in the Android Studio toolbar then click the play button.

Flutter Oauth2 Login Example - prepare for running

Here are the working Flutter apps on the simulator look like.

Flutter Oauth2 Login Example - demo 1
Flutter Oauth2 Login Example - demo2

That it's, the Flutter Oauth2 Login Example. You can get the full source code from our GitHub.

That is just the basic. If you need more deep learning about Flutter, Dart, or related you can take the following cheap course:

Thanks!

Loading…