DEV Community

Cover image for Authentication in Flutter | User Interface Design
Nibesh Khadka
Nibesh Khadka

Posted on • Edited on

Authentication in Flutter | User Interface Design

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 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 
Enter fullscreen mode Exit fullscreen mode

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.dart 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 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 emal 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, ); } } 
Enter fullscreen mode Exit fullscreen mode

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 obscuraction 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 
Enter fullscreen mode Exit fullscreen mode
  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're created a separate file auth_validators.dart 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 presence and position of @ // #2 String? emailValidator(String? val) { final String email = val as String; // If length of email is <=3 then its invlaid // #3 if (email.length <= 3) return emailErrMsg; // Check if it has @ // # 4 final hasAtSymbol = email.contains('@'); // find 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 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; } } 
Enter fullscreen mode Exit fullscreen mode

Inside the auth_validators.dart, we create a 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 returns 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, 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; } 
Enter fullscreen mode Exit fullscreen mode

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 field is empty // Or if thier 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 error message // #2 if (firstPassword != secondPassword) return confirmPasswordErrMsg; return null; } 
Enter fullscreen mode Exit fullscreen mode

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 presence and position of @ String? emailValidator(String? val) { final String email = val as String; // If length of email is <=3 then its invlaid if (email.length <= 3) return emailErrMsg; // Check if it has @ final hasAtSymbol = email.contains('@'); // find position of @ final indexOfAt = email.indexOf('@'); // Check numbers of @ final numbersOfAt = "@".allMatches(email).length; // Valid if has @ if (!hasAtSymbol) return emailErrMsg; // and if 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 field is empty // Or if thier 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 error message if (firstPassword != secondPassword) return confirmPasswordErrMsg; return null; } } 
Enter fullscreen mode Exit fullscreen mode

Homework on Validators

Here's something for you to practice.

  1. xyz..abc@mail.com 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.dart file which will be displayed in auth_screen.dart file.

auth_form_widget.dart

 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(), ), ); } } 
Enter fullscreen mode Exit fullscreen mode

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.dart

 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 gards 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() ]), ), ), ), ), ); } } 
Enter fullscreen mode Exit fullscreen mode

Here is auth_screen.dart, 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 user 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 
Enter fullscreen mode Exit fullscreen mode

It's time to add auth_screen to app_router.dart

Connect Auth Screen to Router

app_router.dart

 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()), ], 
Enter fullscreen mode Exit fullscreen mode

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.dart file that'll take us to auth_screen.
user_drawer.dart

 ............... 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 temporarry 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"); }), ], ..... 
Enter fullscreen mode Exit fullscreen mode

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.dart file is really long. So, lets go over it few pieces at a time first.

Instantiate AuthValidators inside _AuthFormWidgetState class.

 // Instantiate validator final AuthValidators authValidator = AuthValidators(); 
Enter fullscreen mode Exit fullscreen mode

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; 
Enter fullscreen mode Exit fullscreen mode

Create obscure text bool

 // to obscure text default value is false bool obscureText = true; 
Enter fullscreen mode Exit fullscreen mode

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 signin mode bool registerAuthMode = false; 
Enter fullscreen mode Exit fullscreen mode

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(); } 
Enter fullscreen mode Exit fullscreen mode

Password Visibility Handler

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

 void toggleObscureText() { setState(() { obscureText = !obscureText; }); } 
Enter fullscreen mode Exit fullscreen mode

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, )); } 
Enter fullscreen mode Exit fullscreen mode

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: [],) ), ); } 
Enter fullscreen mode Exit fullscreen mode

Input Widgets

It's time to create our email, username, password, and confirm password input forms. Inside 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, ), 
Enter fullscreen mode Exit fullscreen mode

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, ), 
Enter fullscreen mode Exit fullscreen mode
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, ), 
Enter fullscreen mode Exit fullscreen mode
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, ), 
Enter fullscreen mode Exit fullscreen mode

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 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), ), ), ], ), 
Enter fullscreen mode Exit fullscreen mode

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"), ) ], ) 
Enter fullscreen mode Exit fullscreen mode

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 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), ), ), ), 
Enter fullscreen mode Exit fullscreen mode

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.dart 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 to toggle between register and sigin 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"), ) ], ) ], ), ), ); } } 
Enter fullscreen mode Exit fullscreen mode

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 personal 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

Top comments (0)