Authentication in Flutter  | User Interface Design

Authentication in Flutter | User Interface Design

Flutter App Development Tutorial | part - V

Create a dynamic Input widget, a form that can transform into both sign-in and registration based on the authentication mode, animate the transition between registration and sign-in form, and validate user input.

Introduction

Hello and welcome to Khadka's Coding Lounge. This is Nibesh from Khadka's Coding Lounge. First of all, a special thanks to all the followers and subscribers of this blog series. So, far in this series, we made Splash Screen, User Onboarding System, Global Theme, and Custom widgets like the app bar, bottom navigation bar, and a drawer for our application.

Today's blog is one of the most important parts of this blog series as you can tell from the title because now, according to our user-flow screen given below, we have to authenticate the user.

App Launch Flow

Authentication User FLow

Authentication is a basic yet very important aspect of any application regardless of platform. Serverless/Headless apps are on trend these days. Among them, Google's Firebase is one of the popular ones, especially for mobile applications. In this chapter of the series, we'll create an authentication UI. We'll create a dynamic input form widget to be more efficient. We'll also write some validations for the input, and animate sign-in <-> registration form transitions.

You can find code up until from here.

Create Authentication Form In Flutter

We're going to make a simple form. For registration, we'll have four inputs: email, username, password, and confirm password inputs, while the sign-in form only uses two inputs: email and password.

Let's create relevant files and folders in our project.

# cursor on the root folder
# create folders
mkdir lib/screens/auth lib/screens/auth/widgets lib/screens/auth/providers  lib/screens/auth/utils

# Create files
touch lib/screens/auth/auth_screen.dart   lib/screens/auth/providers/auth_provider.dart lib/screens/auth/widgets/text_from_widget.dart  lib/screens/auth/widgets/auth_form_widget.dart lib/screens/auth/utils/auth_validators.dart lib/screens/auth/utils/auth_utils.dart

Create Dynamic Text Form Widget

Before we create our dynamic text form field widget, let's go over the details of how dynamic it's going to be.

Auth Form Dynamic

We'll make our dynamic text form widget in text_from_widget file.

import 'package:flutter/material.dart';

class DynamicInputWidget extends StatelessWidget {
  const DynamicInputWidget(
      {required this.controller,
      required this.obscureText,
      required this.focusNode,
      required this.toggleObscureText,
      required this.validator,
      required this.prefIcon,
      required this.labelText,
      required this.textInputAction,
      required this.isNonPasswordField,
      Key? key})
      : super(key: key);

  // bool to check if the text field is for password or not
  final bool isNonPasswordField;
  // Controller for the text field
  final TextEditingController controller;
  // Functio to toggle Text obscuractio on the password text field
  final VoidCallback? toggleObscureText;
  // to obscure text or not bool
  final bool obscureText;
  // FocusNode for input
  final FocusNode focusNode;
  // Validator function
  final String? Function(String?)? validator;
  // Prefix icon for input form
  final Icon prefIcon;
  // label for input form
  final String labelText;
  // The keyword action to display
  final TextInputAction textInputAction;

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: controller,
      decoration: InputDecoration(
        // Input with border outlined
        border: const OutlineInputBorder(
          // Make border edge circular
          borderRadius: BorderRadius.all(Radius.circular(10.0)),
        ),
        label: Text(labelText),
        prefixIcon: prefIcon,
        suffixIcon: IconButton(
          onPressed: toggleObscureText,
          // If is non-password filed like email the suffix icon will be null
          icon: isNonPasswordField
              ? const Icon(null)
              : obscureText
                  ? const Icon(Icons.visibility)
                  : const Icon(Icons.visibility_off),
        ),
      ),
      focusNode: focusNode,
      textInputAction: textInputAction,
      obscureText: obscureText,
      validator: validator,
      // onSaved: passwordVlidator,
    );
  }
}

Let's go over a few important fields of our dynamic input widget class.


// bool to check if the text field is for password or not
  final bool isNonPasswordField; //# 1
   // Function to toggle Text obscuration on password text field
  final VoidCallback? toggleObscureText; //# 2
  // to obscure text or not bool
  final bool obscureText;
   // Validator function 
  final String? Function(String?)? validator; //# 3
  1. We are checking if the input type is a password or not. We're going to need that to determine whether to display a suffix icon that toggles the visibility of the password.
  2. Boolean obscure text will change the password's visibility from asterisks to strings and vice-versa.
  3. Validator is a function of a property validator in TextFormField. It returns null when valid and a custom string when invalid.

Input Validation in Flutter

Now, that our dynamic input is ready. Let's write some validators before we move on to create our form. We've created a separate file auth_validators for this sole purpose. We'll write three functions, which will separately validate Email, Password, and Confirm Passwords for user input.

Create Email Validator in Flutter

class AuthValidators {
  // Create error messages to send.
 // #1
  static const String emailErrMsg =  "Invalid Email Address, Please provide a valid email.";
  static const String passwordErrMsg = "Password must have at least 6 characters.";
  static const String confirmPasswordErrMsg = "Two passwords don't match.";

// A simple email validator that  checks the presence and position of @
// #2
  String? emailValidator(String? val) {
    final String email = val as String;

    // If the length of an email is <=3 then its invalid
   // #3
    if (email.length <= 3) return emailErrMsg;

    // Check if it has @
  // # 4
    final hasAtSymbol = email.contains('@');

    // find the position of @
    // # 5
    final indexOfAt = email.indexOf('@');

    // Check numbers of @
    // # 6
    final numbersOfAt = "@".allMatches(email).length;

    // Valid if has @
  // # 7
    if (!hasAtSymbol) return emailErrMsg;

    // and  if the number of @ is only 1
  // # 8
    if (numbersOfAt != 1) return emailErrMsg;

    //and if  '@' is not the first or last character
  // # 9
    if (indexOfAt == 0 || indexOfAt == email.length - 1) return emailErrMsg;

    // Else its valid
    return null;
  }
}

Inside the auth_validators, we create an AuthValidators class.

  1. Then we created three error messages that'll be sent as output to the relevant error.
  2. Email Validator Function that takes input value from its input form.
  3. Email is invalid if its length <=3.
  4. (4&7) Checks if there's a @ symbol and return the error message on absence.
  5. (5&9) Checks the position of @. If the position is in the beginning or at the end, that means the email is invalid.
  6. (6&8) If the number of @ is more or less than one, then the input is an invalid email.

That was a very simple validator, where we checked four things: if the input for email is empty, the length of the email input is less than 4, and the position and number @ in the input.

Reminder: Do not change the order of return statements. It will cause an error.

Create Password Validator in Flutter

We'll continue with the second validator for the password. We'll only validate the password if it's not empty and its length is greater than 5. So, once again inside AuthValidator class, we'll create a new function passwordValidator.

// Password validator
  String? passwordVlidator(String? val) {
    final String password = val as String;

    if (password.isEmpty || password.length <= 5) return passwordErrMsg;

    return null;
  }

Create Confirm Password Validator in Flutter

During registration, there will be two password input fields, the second one is to confirm the given password. Our confirmPassword validator will take two inputs: the original password and the second password.

// Confirm password
  String? confirmPasswordValidator(String? val, firstPasswordInpTxt) {
    final String firstPassword = firstPasswordInpTxt;
    final String secondPassword = val as String;
    // If either of the password fields is empty
    // Or if their length do not match then we don't need to compare their content
// #1
    if (firstPassword.isEmpty || 
        secondPassword.isEmpty ||
        firstPassword.length != secondPassword.length) {
      return confirmPasswordErrMsg;
    }

    //  If two passwords do not match then send an error message
// #2
    if (firstPassword != secondPassword) return confirmPasswordErrMsg;

    return null;
  }

For password confirmation, we checked:

  1. If either of the two passwords is empty or if their lengths don't match each other. If so, then return the error message.
  2. The second condition compares the content of two passwords, if they don't match then returns the error message.

Like this our three validators are ready for action.

Altogether AuthValidators Class looks like this.

class AuthValidators {
  // Create error messages to send.
  static const String emailErrMsg =
      "Invalid Email Address, Please provide a valid email.";
  static const String passwordErrMsg =
      "Password must have at least 6 characters.";
  static const String confirmPasswordErrMsg = "Two passwords don't match.";

// A simple email validator that  checks the presence and position of @
  String? emailValidator(String? val) {
    final String email = val as String;

    // If length of email is <=3 then its invalid
    if (email.length <= 3) return emailErrMsg;

    // Check if it has @
    final hasAtSymbol = email.contains('@');
    // find the position of @
    final indexOfAt = email.indexOf('@');
    // Check numbers of @
    final numbersOfAt = "@".allMatches(email).length;

    // Valid if has @
    if (!hasAtSymbol) return emailErrMsg;

    // and  if the number of @ is only 1
    if (numbersOfAt != 1) return emailErrMsg;

    //and if  '@' is not first or last character
    if (indexOfAt == 0 || indexOfAt == email.length - 1) return emailErrMsg;

    // Else its valid
    return null;
  }

  // Password validator
  String? passwordVlidator(String? val) {
    final String password = val as String;

    if (password.isEmpty || password.length <= 5) return passwordErrMsg;

    return null;
  }

  // Confirm password
  String? confirmPasswordValidator(String? val, firstPasswordInpTxt) {
    final String firstPassword = firstPasswordInpTxt;
    final String secondPassword = val as String;
    // If either of the password fields is empty
    // Or if their length do not match then we don't need to compare their content
    if (firstPassword.isEmpty ||
        secondPassword.isEmpty ||
        firstPassword.length != secondPassword.length) {
      return confirmPasswordErrMsg;
    }

    //  If two passwords do not match then send an error message
    if (firstPassword != secondPassword) return confirmPasswordErrMsg;

    return null;
  }
}

Homework on Validators

Here's something for you to practice.

  1. is an invalid email address because symbols(_, ., -) should always be followed by letters or numbers. Can you write a validator for this error?
  2. On the password validator, check that it should contain both numbers and letters to be valid.
  3. Add a username validator, check that it should not be the same as an email address. By the way, the username is the optional input.

How to create Register & Sign in Form in Flutter?

Auth Form Widget: Temporary

Now, that we've made our dynamic input as well as validator it's time to put them together to create an authentication form visuals. We'll create our form widget in the auth_form_widget file which will be displayed in the auth_screen file.

auth_form_widget

import 'package:flutter/material.dart';

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

  @override
  State<AuthFormWidget> createState() => _AuthFormWidgetState();
}

class _AuthFormWidgetState extends State<AuthFormWidget> {
  // Define Form key
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Form(
        key: _formKey,
        child: TextFormField(),
      ),
    );
  }
}

In flutter Form class can act as a container to display TextFormFields(if they are more than one). It also requires a FormState which can be obtained via the global key of type FormState as we did just now. Before this form widget bulks up let's connect it to the auth_screen and display it on the app. We'll change it later on after connecting auth screen to the router.

Authentication Screen

auth_screen

import 'package:flutter/material.dart';
import 'package:temple/globals/settings/router/utils/router_utils.dart';
import 'package:temple/screens/auth/widgets/auth_form_widget.dart';

class AuthScreen extends StatelessWidget {
  const AuthScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(APP_PAGE.auth.routePageTitle)),
      body:
      // Safe area prevents safe guards widgets to go beyond device edges
       SafeArea(
        //===========//
        // to dismiss keyword on tap outside use listener
        child: Listener(
          onPointerDown: (PointerDownEvent event) =>
              FocusManager.instance.primaryFocus?.unfocus(),
          //===========//
          child: SingleChildScrollView(
            child: SizedBox(
              width: double.infinity,
              child: Column(children: [
                // Display a welcome user image
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Image.asset(
                    'assets/AuthScreen/WelcomeScreenImage_landscape_2.png',
                    fit: BoxFit.fill,
                  ),
                ),
                const AuthFormWidget()
              ]),
            ),
          ),
        ),
      ),
    );
  }
}

Here is auth_screen, this is it, we'll not make any changes in the future.

Since the app is only accessible to registered users, the authentication screen will be the first screen our users will be prompted to after onboarding. So, we don't need to display user_drawer or the bottom navbar.

Dismissal of the active keyboard, when tapped outside, is an important characteristic to have. Listener class responds to gesture events like a mouse click, tap, etc. Together with FocusManager we can track the focus node tree and unfocus the active keyboard. You might be wondering why am I using it on auth_screen as a whole instead of auth_form_widget. That's because gesture detector events should cover the whole visible area which in this case is SingleChildScrollView.

Next, is the image section, if you're using the source code from the GitHub repo image should already be in the AuthScreen folder inside assets. Let's add the path in the pubspec file.

assets:
    # Splash Screens
    - assets/splash/om_splash.png
    - assets/splash/om_lotus_splash.png
    - assets/splash/om_lotus_splash_1152x1152.png
    # Onboard Screens
    - assets/onboard/FindTemples.png
    - assets/onboard/FindVenues.png
// New line
    # Auth Screens
    - assets/AuthScreen/WelcomeScreenImage_landscape_2.png

It's time to add auth_screen to app_router.

Connect Auth Screen to Router

app_router

 routes: [
        GoRoute(
          path: APP_PAGE.home.routePath,
          name: APP_PAGE.home.routeName,
          builder: (context, state) => const Home(),
        ),
        // Add the onboard Screen
        GoRoute(
            path: APP_PAGE.onboard.routePath,
            name: APP_PAGE.onboard.routeName,
            builder: (context, state) => const OnBoardScreen()),
        // New line from here
        // Add Auth Screen on Go Router
        GoRoute(
            path: APP_PAGE.auth.routePath,
            name: APP_PAGE.auth.routeName,
            builder: (context, state) => const AuthScreen()),
      ],

Temporary Navigation To AuthScreen

We're yet to write a backend/business logic in this blog. But we do have to test UI. So, let's create a temporary link in our user_drawer file that'll take us to auth_screen.

user_drawer

...............
 actions: [

        ListTile(
            leading: Icon(
              Icons.person_outline_rounded,
              color: Theme.of(context).colorScheme.secondary,
            ),
            title: const Text('User Profile'),
            onTap: () {
              print("User Profile Button Pressed");
            }),
 // ============================//
    // A temporary link to auth screen
        ListTile(
            leading: Icon(
              Icons.login,
              color: Theme.of(context).colorScheme.secondary,
            ),
            title: const Text('Register/Login'),
            onTap: () => GoRouter.of(context).goNamed(APP_PAGE.auth.routeName)),
 // ============================//
        ListTile(
            leading: Icon(
              Icons.logout,
              color: Theme.of(context).colorScheme.secondary,
            ),
            title: const Text('Logout'),
            onTap: () {
              print("Log Out Button Pressed");
            }),
      ],
.....

Save it all and run the app, navigate to auth_screen from the user drawer(press the person icon for the drawer), and you'll see auth screen. Now that we can see the authentication screen, let's create a full-fledged auth form.

A Complete Auth Form Widget

The codes for auth_form_widget file is really long. So, let's go over it a few pieces at a time first.

Instantiate AuthValidators inside _AuthFormWidgetState class.

  // Instantiate validator
  final AuthValidators authValidator = AuthValidators();

Create controllers and focus nodes.

// controllers 
  late TextEditingController emailController;
  late TextEditingController usernameController;
  late TextEditingController passwordController;
  late TextEditingController confirmPasswordController;

// create focus nodes
  late FocusNode emailFocusNode;
  late FocusNode usernameFocusNode;
  late FocusNode passwordFocusNode;
  late FocusNode confirmPasswordFocusNode;

Create obscure text bool

// to obscure text default value is false
  bool obscureText = true;

Auth Mode

Instead of creating two separate screens for registering and signing in, we'll just toggle between the few inputs displayed, on and off. For that, let's create a boolean to control authMode.

 // This will require toggling between register and sign in mode
  bool registerAuthMode = false;

Instantiate and Dispose

Instantiate all the text editing controllers and focus nodes on initState function. Similarly, these all also need to be disposed of once done so let's do that as well with the dispose method.

@override
  void initState() {
    super.initState();
    emailController = TextEditingController();
    usernameController = TextEditingController();
    passwordController = TextEditingController();
    confirmPasswordController = TextEditingController();

    emailFocusNode = FocusNode();
    usernameFocusNode = FocusNode();
    passwordFocusNode = FocusNode();
    confirmPasswordFocusNode = FocusNode();
  }

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

    emailController.dispose();
    usernameController.dispose();
    passwordController.dispose();
    confirmPasswordController.dispose();

    emailFocusNode.dispose();
    usernameFocusNode.dispose();
    passwordFocusNode.dispose();
    confirmPasswordFocusNode.dispose();
  }

Password Visibility Handler

Create a function that'll toggle the password's visibility on the relevant icon tap.

 void toggleObscureText() {
    setState(() {
      obscureText = !obscureText;
    });
  }

Snackbar Widget

Let's create a snack bar to pop info on various circumstances.

// Create a scaffold messanger
  SnackBar msgPopUp(msg) {
    return SnackBar(
        content: Text(
      msg,
      textAlign: TextAlign.center,
    ));
  }

Prepare for List of Widgets In Column

Let's change the child of Form we're returning to a column.

@override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Form(
        key: _formKey,
        child: Column(children: [],)
      ),
    );
  }

Input Widgets

It's time to create our email, username, and password, and confirm password input forms. Inside the column's children for from widget, we'll add inputs one by one.

Email Input
    // Email
            DynamicInputWidget(
              controller: emailController,
              obscureText: false,
              focusNode: emailFocusNode,
              toggleObscureText: null,
              validator: authValidator.emailValidator,
              prefIcon: const Icon(Icons.mail),
              labelText: "Enter Email Address",
              textInputAction: TextInputAction.next,
              isNonPasswordField: true,
            ),
            const SizedBox(
              height: 20,
            ),

Make sure to import DynamicInput Widget

Username Input
    // Username
              DynamicInputWidget(
                controller: usernameController,
                obscureText: false,
                focusNode: usernameFocusNode,
                toggleObscureText: null,
                validator: null,
                prefIcon: const Icon(Icons.person),
                labelText: "Enter Username(Optional)",
                textInputAction: TextInputAction.next,
                isNonPasswordField: true,
              ),
               const SizedBox(
                height: 20,
              ),
Password Input
// password
  DynamicInputWidget(
              controller: passwordController,
              labelText: "Enter Password",
              obscureText: obscureText,
              focusNode: passwordFocusNode,
              toggleObscureText: toggleObscureText,
              validator: authValidator.passwordVlidator,
              prefIcon: const Icon(Icons.password),
              textInputAction: registerAuthMode
                  ? TextInputAction.next
                  : TextInputAction.done,
              isNonPasswordField: false,
            ),

            const SizedBox(
              height: 20,
            ),
Confirm Password Input
// confirm password
DynamicInputWidget(
                controller: confirmPasswordController,
                focusNode: confirmPasswordFocusNode,
                isNonPasswordField: false,
                labelText: "Confirm Password",
                obscureText: obscureText,
                prefIcon: const Icon(Icons.password),
                textInputAction: TextInputAction.done,
                toggleObscureText: toggleObscureText,
                validator: (val) => authValidator.confirmPasswordValidator(
                    val, passwordController.text),
              ),

              const SizedBox(
                height: 20,
              ),

Regsiter/SignIn Button

Later on, we'll also need to create and toggle functions to register and sign in once we set up the firebase back-end. For now, we'll just toggle the register/sign-in texts of the elevated button.

// Toggle register/singin button text. Later on, we'll also need to toggle the register or signin function
Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                  onPressed: () {},
                  child: const Text('Cancel'),
                ),
                const SizedBox(
                  width: 20,
                ),
                ElevatedButton(
                  onPressed: () {},
                  child: Text(registerAuthMode ? 'Register' : 'Sign In'),
                  style: ButtonStyle(
                    elevation: MaterialStateProperty.all(8.0),
                  ),
                ),
              ],
            ),

Toggle Auth Mode

Manage authentication mode with setState().

 Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(registerAuthMode
                    ? "Already Have an account?"
                    : "Don't have an account yet?"),
                TextButton(
                  onPressed: () =>
                      setState(() => registerAuthMode = !registerAuthMode),
                  child: Text(registerAuthMode ? "Sign In" : "Regsiter"),
                )
              ],
            )

Register/Sigin Animated Transition

If you save the file and run it. You probably notice the toggle doesn't work. That's because we haven't implemented an animated transition yet. Two input widgets: username and confirm password widgets need to be hidden during the sign-in process but visible on registration. So, we'll use the toggle visibility base on the value of the variable registerAuthMode we created earlier. We'll use animation for a smooth transition during the toggle.

AnimatedContainer class can be used for animation, while AnimatedOpacity class can be used for fade in/out widgets. With the combination of these two, we'll toggle input with animated opacity while the animated container will squeeze/fill the space occupied by input smoothly.

So, let's animate the username, sized-box widget following it, and confirm-password input widget.

    // Username
              AnimatedContainer(
                duration: const Duration(milliseconds: 500),
                height: registerAuthMode ? 65 : 0,
                child: AnimatedOpacity(
                  duration: const Duration(milliseconds: 500),
                  opacity: registerAuthMode ? 1 : 0,
                  child: DynamicInputWidget(
                    controller: usernameController,
                    obscureText: false,
                    focusNode: usernameFocusNode,
                    toggleObscureText: null,
                    validator: null,
                    prefIcon: const Icon(Icons.person),
                    labelText: "Enter Username(Optional)",
                    textInputAction: TextInputAction.next,
                    isNonPasswordField: true,
                  ),
                ),
              ),
// We'll also need to fade in/out sizedbox
              AnimatedOpacity(
              duration: const Duration(milliseconds: 500),
              opacity: registerAuthMode ? 1 : 0,
              child: const SizedBox(
                height: 20,
              ),
            ),

// Confirm password 
 AnimatedContainer(
                duration: const Duration(milliseconds: 500),
                height: registerAuthMode ? 65 : 0,
                child: AnimatedOpacity(
                  duration: const Duration(milliseconds: 500),
                  opacity: registerAuthMode ? 1 : 0,
                  child: DynamicInputWidget(
                    controller: confirmPasswordController,
                    focusNode: confirmPasswordFocusNode,
                    isNonPasswordField: false,
                    labelText: "Confirm Password",
                    obscureText: obscureText,
                    prefIcon: const Icon(Icons.password),
                    textInputAction: TextInputAction.done,
                    toggleObscureText: toggleObscureText,
                    validator: (val) => authValidator.confirmPasswordValidator(
                        val, passwordController.text),
                  ),
                ),
              ),

After this, you'll be able to see two inputs on the authentication screen, cause sign-in mode is our default mode. Now, our Form Widget UI is ready. Don't be confused, you can find all auth_form_widget as a whole down below.

import 'package:flutter/material.dart';
import 'package:temple/screens/auth/utils/auth_validators.dart';
import 'package:temple/screens/auth/widgets/text_from_widget.dart';

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

  @override
  State<AuthFormWidget> createState() => _AuthFormWidgetState();
}

class _AuthFormWidgetState extends State<AuthFormWidget> {
  // Define Form key
  final _formKey = GlobalKey<FormState>();

  // Instantiate validator
  final AuthValidators authValidator = AuthValidators();

// controllers
  late TextEditingController emailController;
  late TextEditingController usernameController;
  late TextEditingController passwordController;
  late TextEditingController confirmPasswordController;

// create focus nodes
  late FocusNode emailFocusNode;
  late FocusNode usernameFocusNode;
  late FocusNode passwordFocusNode;
  late FocusNode confirmPasswordFocusNode;

  // to obscure text default value is false
  bool obscureText = true;
  // This will require toggling between register and sign-in mode
  bool registerAuthMode = false;

// Instantiate all the *text editing controllers* and focus nodes on *initState* function
  @override
  void initState() {
    super.initState();
    emailController = TextEditingController();
    usernameController = TextEditingController();
    passwordController = TextEditingController();
    confirmPasswordController = TextEditingController();

    emailFocusNode = FocusNode();
    usernameFocusNode = FocusNode();
    passwordFocusNode = FocusNode();
    confirmPasswordFocusNode = FocusNode();
  }

// These all need to be disposed of once done so let's do that as well.
  @override
  void dispose() {
    super.dispose();

    emailController.dispose();
    usernameController.dispose();
    passwordController.dispose();
    confirmPasswordController.dispose();

    emailFocusNode.dispose();
    usernameFocusNode.dispose();
    passwordFocusNode.dispose();
    confirmPasswordFocusNode.dispose();
  }

// Create a function that'll toggle the password's visibility on the relevant icon tap.
  void toggleObscureText() {
    setState(() {
      obscureText = !obscureText;
    });
  }

// Let's create a snack bar to pop info on various circumstances.
// Create a scaffold messanger
  SnackBar msgPopUp(msg) {
    return SnackBar(
        content: Text(
      msg,
      textAlign: TextAlign.center,
    ));
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Form(
        key: _formKey,
        child: Column(
          children: [
            // Email
            DynamicInputWidget(
              controller: emailController,
              obscureText: false,
              focusNode: emailFocusNode,
              toggleObscureText: null,
              validator: authValidator.emailValidator,
              prefIcon: const Icon(Icons.mail),
              labelText: "Enter Email Address",
              textInputAction: TextInputAction.next,
              isNonPasswordField: true,
            ),
            const SizedBox(
              height: 20,
            ),
            // Username
            AnimatedContainer(
              duration: const Duration(milliseconds: 500),
              height: registerAuthMode ? 65 : 0,
              child: AnimatedOpacity(
                duration: const Duration(milliseconds: 500),
                opacity: registerAuthMode ? 1 : 0,
                child: DynamicInputWidget(
                  controller: usernameController,
                  obscureText: false,
                  focusNode: usernameFocusNode,
                  toggleObscureText: null,
                  validator: null,
                  prefIcon: const Icon(Icons.person),
                  labelText: "Enter Username(Optional)",
                  textInputAction: TextInputAction.next,
                  isNonPasswordField: true,
                ),
              ),
            ),

            AnimatedOpacity(
              duration: const Duration(milliseconds: 500),
              opacity: registerAuthMode ? 1 : 0,
              child: const SizedBox(
                height: 20,
              ),
            ),

            DynamicInputWidget(
              controller: passwordController,
              labelText: "Enter Password",
              obscureText: obscureText,
              focusNode: passwordFocusNode,
              toggleObscureText: toggleObscureText,
              validator: authValidator.passwordVlidator,
              prefIcon: const Icon(Icons.password),
              textInputAction: registerAuthMode
                  ? TextInputAction.next
                  : TextInputAction.done,
              isNonPasswordField: false,
            ),

            const SizedBox(
              height: 20,
            ),

            AnimatedContainer(
              duration: const Duration(milliseconds: 500),
              height: registerAuthMode ? 65 : 0,
              child: AnimatedOpacity(
                duration: const Duration(milliseconds: 500),
                opacity: registerAuthMode ? 1 : 0,
                child: DynamicInputWidget(
                  controller: confirmPasswordController,
                  focusNode: confirmPasswordFocusNode,
                  isNonPasswordField: false,
                  labelText: "Confirm Password",
                  obscureText: obscureText,
                  prefIcon: const Icon(Icons.password),
                  textInputAction: TextInputAction.done,
                  toggleObscureText: toggleObscureText,
                  validator: (val) => authValidator.confirmPasswordValidator(
                      val, passwordController.text),
                ),
              ),
            ),
            const SizedBox(
              height: 20,
            ),

            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                  onPressed: () {},
                  child: const Text('Cancel'),
                ),
                const SizedBox(
                  width: 20,
                ),
                ElevatedButton(
                  onPressed: () {},
                  child: Text(registerAuthMode ? 'Register' : 'Sign In'),
                  style: ButtonStyle(
                    elevation: MaterialStateProperty.all(8.0),
                  ),
                ),
              ],
            ),

            const SizedBox(
              height: 20,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(registerAuthMode
                    ? "Already Have an account?"
                    : "Don't have an account yet?"),
                TextButton(
                  onPressed: () =>
                      setState(() => registerAuthMode = !registerAuthMode),
                  child: Text(registerAuthMode ? "Sign In" : "Regsiter"),
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

Our validator's still not going to work, because we still haven't handled what to do on form submission. But let's check out the result of our hard work.

KCl

Summary

Let's summarize what we did so far.

  1. Created Dynamic Input Widget.
  2. Wrote our validators for the inputs we'll be taking in.
  3. Create both sign-in and registration forms on one screen.
  4. Animated the transition between Registration and Sign In authentication.

Show Support

That's it for today. We'll work on Firebase Flutter Set-Up in the next section. If you have any questions then leave them in the comment section.

Thank you for your time. Don't forget to give a like and share the article. Hopefully, you'll also subscribe to the publication to get notified of the next upload. This is Nibesh Khadka from Khadka's Coding Lounge. We are a freelancing agency that makes high-value websites and mobile applications.

Like, Share, Comment, and Subscribe