Popular Searches
Popular Course Categories
Popular Courses

Building a practical authentication interface

Building a practical authentication interface

Flutter Forms & User Input

Building a Practical Authentication Interface in Flutter – Detailed Notes

A practical authentication interface in Flutter combines Login, Registration, Forgot Password, validation, password visibility, loading states, navigation, error handling, and backend integration preparation into a complete user authentication experience.

This module focuses on building an authentication interface that is reusable, responsive, user-friendly, and ready to connect with an authentication backend.


1. What Is an Authentication Interface?

An authentication interface is a collection of screens and UI components used to identify users and manage account access.

Common Authentication Screens

  • Login Screen
  • Registration Screen
  • Forgot Password Screen
  • Reset Password Screen
  • Email or OTP Verification Screen
  • Home/Dashboard Screen after successful authentication

Basic Authentication Flow

Application Start
      ↓
Login Screen
      ↓
Existing User?
 ┌────┴────┐
Yes        No
 ↓          ↓
Login    Registration
 ↓          ↓
Validate   Validate
 ↓          ↓
Authentication Request
      ↓
Success?
 ┌────┴────┐
Yes        No
 ↓          ↓
Home     Show Error

2. Main Components of a Practical Authentication UI

Component Purpose
Login Form Collects credentials from an existing user.
Registration Form Collects information required to create an account.
Validation Checks whether entered data follows the required rules.
Password Visibility Allows users to show or hide password text.
Loading State Indicates that an authentication request is processing.
Error Handling Displays useful feedback when authentication fails.
Navigation Moves users between authentication screens.
Session Handling Maintains authenticated state after successful login.
Responsive Layout Makes authentication screens usable across different screen sizes.

3. Recommended Flutter Widgets

  • Scaffold – Provides the basic screen structure.
  • SafeArea – Helps keep content away from system UI areas.
  • SingleChildScrollView – Helps prevent overflow when the keyboard appears.
  • Form – Groups and manages form fields.
  • TextFormField – Creates validated text input fields.
  • TextEditingController – Reads and controls user-entered text.
  • FocusNode – Manages keyboard focus.
  • ElevatedButton – Creates primary actions.
  • TextButton – Creates secondary actions and links.
  • Checkbox – Handles options such as Remember Me or Terms acceptance.
  • InputDecoration – Controls text field appearance.
  • Navigator – Handles screen navigation.
  • SnackBar – Provides temporary feedback messages.

Flutter's Form widget can group multiple form fields and expose operations such as saving, resetting, and validating them. TextFormField integrates a text field with form-field behavior and supports validators. :contentReference[oaicite:0]{index=0}


4. Suggested Project Structure

lib/
├── main.dart
├── screens/
│   ├── login_screen.dart
│   ├── register_screen.dart
│   ├── forgot_password_screen.dart
│   └── home_screen.dart
├── widgets/
│   ├── auth_text_field.dart
│   └── auth_button.dart
├── services/
│   └── auth_service.dart
├── models/
│   └── user_model.dart
└── validators/
    └── validators.dart

This structure separates screens, reusable UI components, authentication logic, data models, and validation functions.


5. Designing the Login Screen

A practical Login screen normally contains:

  • Application logo
  • Welcome message
  • Email field
  • Password field
  • Password visibility button
  • Remember Me option
  • Forgot Password link
  • Login button
  • Registration link

Basic Layout

Scaffold
└── SafeArea
    └── SingleChildScrollView
        └── Center
            └── ConstrainedBox
                └── Form
                    ├── Logo
                    ├── Welcome Text
                    ├── Email Field
                    ├── Password Field
                    ├── Remember Me
                    ├── Forgot Password
                    ├── Login Button
                    └── Registration Link

6. Creating the Authentication Form

The following example creates a practical login form with controllers, validation, password visibility, loading state, and navigation.

import 'package:flutter/material.dart';

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

  @override
  State createState() => _LoginScreenState();
}

class _LoginScreenState extends State {
  final _formKey = GlobalKey();
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  bool obscurePassword = true;
  bool rememberMe = false;
  bool isLoading = false;

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const SizedBox(height: 40),
                const Icon(
                  Icons.lock,
                  size: 80,
                ),
                const SizedBox(height: 20),
                const Text(
                  'Welcome Back',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 30,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 8),
                const Text(
                  'Login to continue',
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 30),
                buildEmailField(),
                const SizedBox(height: 16),
                buildPasswordField(),
                buildRememberMe(),
                buildLoginButton(),
                buildRegisterLink(),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget buildEmailField() {
    return TextFormField(
      controller: emailController,
      keyboardType: TextInputType.emailAddress,
      decoration: const InputDecoration(
        labelText: 'Email',
        hintText: 'Enter your email',
        prefixIcon: Icon(Icons.email),
        border: OutlineInputBorder(),
      ),
      validator: (value) {
        if (value == null || value.trim().isEmpty) {
          return 'Please enter your email';
        }

        if (!value.contains('@')) {
          return 'Please enter a valid email';
        }

        return null;
      },
    );
  }

  Widget buildPasswordField() {
    return TextFormField(
      controller: passwordController,
      obscureText: obscurePassword,
      decoration: InputDecoration(
        labelText: 'Password',
        hintText: 'Enter your password',
        prefixIcon: const Icon(Icons.lock),
        suffixIcon: IconButton(
          icon: Icon(
            obscurePassword
                ? Icons.visibility
                : Icons.visibility_off,
          ),
          onPressed: () {
            setState(() {
              obscurePassword = !obscurePassword;
            });
          },
        ),
        border: const OutlineInputBorder(),
      ),
      validator: (value) {
        if (value == null || value.isEmpty) {
          return 'Please enter your password';
        }

        if (value.length < 8) {
          return 'Password must contain at least 8 characters';
        }

        return null;
      },
    );
  }

  Widget buildRememberMe() {
    return CheckboxListTile(
      value: rememberMe,
      contentPadding: EdgeInsets.zero,
      title: const Text('Remember Me'),
      controlAffinity: ListTileControlAffinity.leading,
      onChanged: (value) {
        setState(() {
          rememberMe = value ?? false;
        });
      },
    );
  }

  Widget buildLoginButton() {
    return SizedBox(
      height: 50,
      child: ElevatedButton(
        onPressed: isLoading ? null : login,
        child: isLoading
            ? const CircularProgressIndicator()
            : const Text('Login'),
      ),
    );
  }

  Widget buildRegisterLink() {
    return TextButton(
      onPressed: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => const RegisterScreen(),
          ),
        );
      },
      child: const Text(
        "Don't have an account? Register",
      ),
    );
  }

  Future login() async {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    setState(() {
      isLoading = true;
    });

    try {
      await Future.delayed(
        const Duration(seconds: 2),
      );

      if (!mounted) {
        return;
      }

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Login successful'),
        ),
      );
    } catch (e) {
      if (!mounted) {
        return;
      }

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Login failed'),
        ),
      );
    } finally {
      if (mounted) {
        setState(() {
          isLoading = false;
        });
      }
    }
  }
}

7. Understanding Form Validation

Flutter's recommended form-validation approach is to create a Form with a GlobalKey, add TextFormField validators, and call FormState.validate() before processing the submitted data. :contentReference[oaicite:1]{index=1}

final _formKey = GlobalKey();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'This field is required';
          }
          return null;
        },
      ),
    ],
  ),
)

Validate Before Submission

if (_formKey.currentState!.validate()) {
  // Submit the authentication request.
} else {
  // Validation errors are displayed.
}

8. Creating Reusable Validators

Instead of writing validation logic repeatedly, reusable validator functions can be created.

Email Validator

String? validateEmail(String? value) {
  if (value == null || value.trim().isEmpty) {
    return 'Email is required';
  }

  final emailRegex = RegExp(
    r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
  );

  if (!emailRegex.hasMatch(value.trim())) {
    return 'Enter a valid email address';
  }

  return null;
}

Password Validator

String? validatePassword(String? value) {
  if (value == null || value.isEmpty) {
    return 'Password is required';
  }

  if (value.length < 8) {
    return 'Password must contain at least 8 characters';
  }

  return null;
}

Name Validator

String? validateName(String? value) {
  if (value == null || value.trim().isEmpty) {
    return 'Name is required';
  }

  if (value.trim().length < 2) {
    return 'Enter a valid name';
  }

  return null;
}

9. Using Reusable Validators

TextFormField(
  controller: emailController,
  validator: validateEmail,
  decoration: const InputDecoration(
    labelText: 'Email',
    border: OutlineInputBorder(),
  ),
)

TextFormField(
  controller: passwordController,
  obscureText: true,
  validator: validatePassword,
  decoration: const InputDecoration(
    labelText: 'Password',
    border: OutlineInputBorder(),
  ),
)

10. Building the Registration Interface

A practical registration interface collects the information required to create a new account.

Registration Fields

  • Full Name
  • Email
  • Phone Number
  • Password
  • Confirm Password
  • Terms and Conditions

Registration Example

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

  @override
  State createState() => _RegisterScreenState();
}

class _RegisterScreenState extends State {
  final _formKey = GlobalKey();

  final nameController = TextEditingController();
  final emailController = TextEditingController();
  final phoneController = TextEditingController();
  final passwordController = TextEditingController();
  final confirmPasswordController = TextEditingController();

  bool acceptedTerms = false;
  bool obscurePassword = true;
  bool obscureConfirmPassword = true;
  bool isLoading = false;

  @override
  void dispose() {
    nameController.dispose();
    emailController.dispose();
    phoneController.dispose();
    passwordController.dispose();
    confirmPasswordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Create Account'),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const Text(
                  'Create Your Account',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 28,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 30),
                buildNameField(),
                const SizedBox(height: 16),
                buildEmailField(),
                const SizedBox(height: 16),
                buildPhoneField(),
                const SizedBox(height: 16),
                buildPasswordField(),
                const SizedBox(height: 16),
                buildConfirmPasswordField(),
                const SizedBox(height: 10),
                buildTermsCheckbox(),
                const SizedBox(height: 20),
                buildRegisterButton(),
                const SizedBox(height: 10),
                TextButton(
                  onPressed: () {
                    Navigator.pop(context);
                  },
                  child: const Text(
                    'Already have an account? Login',
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget buildNameField() {
    return TextFormField(
      controller: nameController,
      textCapitalization: TextCapitalization.words,
      decoration: const InputDecoration(
        labelText: 'Full Name',
        prefixIcon: Icon(Icons.person),
        border: OutlineInputBorder(),
      ),
      validator: validateName,
    );
  }

  Widget buildEmailField() {
    return TextFormField(
      controller: emailController,
      keyboardType: TextInputType.emailAddress,
      decoration: const InputDecoration(
        labelText: 'Email',
        prefixIcon: Icon(Icons.email),
        border: OutlineInputBorder(),
      ),
      validator: validateEmail,
    );
  }

  Widget buildPhoneField() {
    return TextFormField(
      controller: phoneController,
      keyboardType: TextInputType.phone,
      decoration: const InputDecoration(
        labelText: 'Phone Number',
        prefixIcon: Icon(Icons.phone),
        border: OutlineInputBorder(),
      ),
      validator: (value) {
        if (value == null || value.trim().isEmpty) {
          return 'Phone number is required';
        }

        if (value.trim().length < 10) {
          return 'Enter a valid phone number';
        }

        return null;
      },
    );
  }

  Widget buildPasswordField() {
    return TextFormField(
      controller: passwordController,
      obscureText: obscurePassword,
      decoration: InputDecoration(
        labelText: 'Password',
        prefixIcon: const Icon(Icons.lock),
        suffixIcon: IconButton(
          icon: Icon(
            obscurePassword
                ? Icons.visibility
                : Icons.visibility_off,
          ),
          onPressed: () {
            setState(() {
              obscurePassword = !obscurePassword;
            });
          },
        ),
        border: const OutlineInputBorder(),
      ),
      validator: validatePassword,
    );
  }

  Widget buildConfirmPasswordField() {
    return TextFormField(
      controller: confirmPasswordController,
      obscureText: obscureConfirmPassword,
      decoration: InputDecoration(
        labelText: 'Confirm Password',
        prefixIcon: const Icon(Icons.lock_outline),
        suffixIcon: IconButton(
          icon: Icon(
            obscureConfirmPassword
                ? Icons.visibility
                : Icons.visibility_off,
          ),
          onPressed: () {
            setState(() {
              obscureConfirmPassword =
                  !obscureConfirmPassword;
            });
          },
        ),
        border: const OutlineInputBorder(),
      ),
      validator: (value) {
        if (value == null || value.isEmpty) {
          return 'Please confirm your password';
        }

        if (value != passwordController.text) {
          return 'Passwords do not match';
        }

        return null;
      },
    );
  }

  Widget buildTermsCheckbox() {
    return CheckboxListTile(
      contentPadding: EdgeInsets.zero,
      value: acceptedTerms,
      title: const Text(
        'I agree to the Terms and Conditions',
      ),
      onChanged: (value) {
        setState(() {
          acceptedTerms = value ?? false;
        });
      },
    );
  }

  Widget buildRegisterButton() {
    return SizedBox(
      height: 50,
      child: ElevatedButton(
        onPressed: isLoading ? null : register,
        child: isLoading
            ? const CircularProgressIndicator()
            : const Text('Register'),
      ),
    );
  }

  Future register() async {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    if (!acceptedTerms) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text(
            'Please accept the Terms and Conditions',
          ),
        ),
      );
      return;
    }

    setState(() {
      isLoading = true;
    });

    try {
      await Future.delayed(
        const Duration(seconds: 2),
      );

      if (!mounted) {
        return;
      }

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Account created successfully'),
        ),
      );
    } finally {
      if (mounted) {
        setState(() {
          isLoading = false;
        });
      }
    }
  }
}

11. Password Visibility Toggle

Password visibility improves usability by allowing users to check what they typed before submitting.

bool obscurePassword = true;

TextFormField(
  obscureText: obscurePassword,
  decoration: InputDecoration(
    labelText: 'Password',
    suffixIcon: IconButton(
      icon: Icon(
        obscurePassword
            ? Icons.visibility
            : Icons.visibility_off,
      ),
      onPressed: () {
        setState(() {
          obscurePassword = !obscurePassword;
        });
      },
    ),
  ),
)

12. Managing User Input with TextEditingController

TextEditingController can retrieve the text entered by a user and can also listen for changes. Controllers should be disposed when they are no longer needed. :contentReference[oaicite:2]{index=2}

final emailController = TextEditingController();

TextFormField(
  controller: emailController,
);

void getEmail() {
  final email = emailController.text.trim();
  print(email);
}

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

13. Handling Password Strength

A registration interface can provide password-strength feedback as the user types.

String getPasswordStrength(String password) {
  if (password.length < 6) {
    return 'Weak';
  }

  final hasUppercase =
      password.contains(RegExp(r'[A-Z]'));

  final hasNumber =
      password.contains(RegExp(r'[0-9]'));

  if (password.length >= 8 &&
      hasUppercase &&
      hasNumber) {
    return 'Strong';
  }

  return 'Medium';
}

Displaying Password Strength

TextFormField(
  controller: passwordController,
  obscureText: true,
  onChanged: (value) {
    setState(() {});
  },
)

Text(
  'Password Strength: ${getPasswordStrength(passwordController.text)}',
)

14. Auto Validation

Flutter supports AutovalidateMode to control when form validation occurs automatically. Available modes include disabled, always, onUserInteraction, onUnfocus, and onUserInteractionIfError. :contentReference[oaicite:3]{index=3}

TextFormField(
  autovalidateMode:
      AutovalidateMode.onUserInteraction,
  validator: validateEmail,
  decoration: const InputDecoration(
    labelText: 'Email',
    border: OutlineInputBorder(),
  ),
)

15. Forgot Password Interface

A practical authentication system should provide a password-recovery path for users who cannot remember their password.

TextButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) =>
            const ForgotPasswordScreen(),
      ),
    );
  },
  child: const Text('Forgot Password?'),
)

Forgot Password Screen

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

  @override
  State createState() =>
      _ForgotPasswordScreenState();
}

class _ForgotPasswordScreenState
    extends State {
  final emailController = TextEditingController();

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Forgot Password'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            const Text(
              'Enter your email address to receive '
              'password reset instructions.',
            ),
            const SizedBox(height: 20),
            TextFormField(
              controller: emailController,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 20),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
                  print(
                    'Reset email requested',
                  );
                },
                child: const Text(
                  'Send Reset Link',
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

16. Creating a Custom Authentication Button

Reusable widgets make the authentication interface easier to maintain.

class AuthButton extends StatelessWidget {
  final String text;
  final VoidCallback? onPressed;
  final bool isLoading;

  const AuthButton({
    super.key,
    required this.text,
    required this.onPressed,
    this.isLoading = false,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      height: 50,
      child: ElevatedButton(
        onPressed: isLoading ? null : onPressed,
        child: isLoading
            ? const CircularProgressIndicator()
            : Text(text),
      ),
    );
  }
}

Using the Custom Button

AuthButton(
  text: 'Login',
  isLoading: isLoading,
  onPressed: login,
)

17. Creating a Custom Authentication Text Field

class AuthTextField extends StatelessWidget {
  final TextEditingController controller;
  final String label;
  final IconData icon;
  final bool obscureText;
  final Widget? suffixIcon;
  final String? Function(String?)? validator;
  final TextInputType? keyboardType;

  const AuthTextField({
    super.key,
    required this.controller,
    required this.label,
    required this.icon,
    this.obscureText = false,
    this.suffixIcon,
    this.validator,
    this.keyboardType,
  });

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: controller,
      obscureText: obscureText,
      validator: validator,
      keyboardType: keyboardType,
      decoration: InputDecoration(
        labelText: label,
        prefixIcon: Icon(icon),
        suffixIcon: suffixIcon,
        border: const OutlineInputBorder(),
      ),
    );
  }
}

Using the Custom Field

AuthTextField(
  controller: emailController,
  label: 'Email',
  icon: Icons.email,
  keyboardType: TextInputType.emailAddress,
  validator: validateEmail,
)

18. Focus Management

Focus management allows users to move naturally between input fields.

final emailFocus = FocusNode();
final passwordFocus = FocusNode();

TextFormField(
  focusNode: emailFocus,
  textInputAction: TextInputAction.next,
  onFieldSubmitted: (_) {
    passwordFocus.requestFocus();
  },
)

TextFormField(
  focusNode: passwordFocus,
  obscureText: true,
  textInputAction: TextInputAction.done,
)

If a screen creates and owns its FocusNode instances, those nodes should be disposed when the screen is disposed.


19. Responsive Authentication Layout

Authentication forms should remain usable on small mobile screens as well as tablets and desktop-sized windows.

Scaffold(
  body: SafeArea(
    child: SingleChildScrollView(
      child: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(
            maxWidth: 500,
          ),
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: authenticationForm,
          ),
        ),
      ),
    ),
  ),
)

Benefits

  • SafeArea helps avoid system UI areas.
  • SingleChildScrollView helps prevent vertical overflow.
  • Center keeps the form visually balanced.
  • ConstrainedBox prevents the form from becoming excessively wide.
  • Responsive padding provides consistent spacing.

20. Handling Keyboard Overflow

Authentication forms can contain several fields. On smaller screens, the keyboard may reduce the available space. A scrollable layout is therefore useful.

SingleChildScrollView(
  padding: const EdgeInsets.all(24),
  child: Form(
    key: _formKey,
    child: Column(
      children: [
        // Authentication fields
      ],
    ),
  ),
)

21. Loading State During Authentication

A loading state provides feedback that an authentication request is currently being processed.

bool isLoading = false;

Future login() async {
  if (!_formKey.currentState!.validate()) {
    return;
  }

  setState(() {
    isLoading = true;
  });

  try {
    await Future.delayed(
      const Duration(seconds: 2),
    );

    print('Authentication completed');
  } finally {
    if (mounted) {
      setState(() {
        isLoading = false;
      });
    }
  }
}

Disable the Button While Loading

ElevatedButton(
  onPressed: isLoading ? null : login,
  child: isLoading
      ? const CircularProgressIndicator()
      : const Text('Login'),
)

22. Handling Authentication Errors

Authentication can fail even when the input passes local validation. For example, credentials may be incorrect or the server may be unavailable.

try {
  // Call your authentication service here.
  await authService.login(
    email: emailController.text.trim(),
    password: passwordController.text,
  );
} catch (e) {
  if (!mounted) {
    return;
  }

  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text(
        'Unable to login. Please try again.',
      ),
    ),
  );
}

23. Authentication Service Example

Authentication logic can be separated from the UI into a service class.

class AuthService {
  Future login({
    required String email,
    required String password,
  }) async {
    // Replace this with your backend API call.
    await Future.delayed(
      const Duration(seconds: 2),
    );

    if (email == '[email protected]' &&
        password == 'password123') {
      return true;
    }

    return false;
  }

  Future register({
    required String name,
    required String email,
    required String password,
  }) async {
    // Replace this with your backend API call.
    await Future.delayed(
      const Duration(seconds: 2),
    );

    return true;
  }
}

The credentials in this example are only demonstration values. A production application should authenticate against a real backend or authentication provider and should not hard-code user credentials.


24. Connecting the Login Screen to AuthService

final authService = AuthService();

Future login() async {
  if (!_formKey.currentState!.validate()) {
    return;
  }

  setState(() {
    isLoading = true;
  });

  try {
    final success = await authService.login(
      email: emailController.text.trim(),
      password: passwordController.text,
    );

    if (!mounted) {
      return;
    }

    if (success) {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => const HomeScreen(),
        ),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text(
            'Invalid email or password',
          ),
        ),
      );
    }
  } finally {
    if (mounted) {
      setState(() {
        isLoading = false;
      });
    }
  }
}

25. Home Screen After Successful Login

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: const Center(
        child: Text(
          'Welcome to the application!',
          style: TextStyle(
            fontSize: 24,
          ),
        ),
      ),
    );
  }
}

26. Using Navigator.pushReplacement

After successful authentication, pushReplacement can be used when the login screen should no longer remain as the immediate previous route.

Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => const HomeScreen(),
  ),
);

This can help prevent a user from simply returning to the login screen with the back button immediately after a successful login.


27. Remember Me Concept

Remember Me is a UI option indicating that the user wants a persistent authentication/session experience.

bool rememberMe = false;

CheckboxListTile(
  value: rememberMe,
  title: const Text('Remember Me'),
  onChanged: (value) {
    setState(() {
      rememberMe = value ?? false;
    });
  },
)

In production applications, session persistence should use secure authentication/session mechanisms. User passwords should not be stored as plain text.


28. Terms and Conditions Validation

if (!_formKey.currentState!.validate()) {
  return;
}

if (!acceptedTerms) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text(
        'Please accept the Terms and Conditions',
      ),
    ),
  );
  return;
}

await register();

29. Email Verification Concept

Some applications require users to verify their email address after registration.

Registration
     ↓
Create Account
     ↓
Send Verification Email
     ↓
User Opens Email
     ↓
Verify Email
     ↓
Account Activated
     ↓
Login

The actual email-verification process should be implemented by the selected backend or authentication provider.


30. OTP Verification Concept

Applications may also use one-time passwords for phone or email verification.

Enter Phone/Email
       ↓
Request OTP
       ↓
OTP Sent
       ↓
Enter OTP
       ↓
Verify OTP
       ↓
Success
       ↓
Continue Authentication

31. Improving the Authentication UI

Visual Improvements

  • Use consistent spacing.
  • Use clear field labels.
  • Use meaningful icons.
  • Use a consistent button style.
  • Provide visible focus states.
  • Keep error messages close to the relevant fields.
  • Use readable font sizes.
  • Maintain sufficient contrast.
  • Keep the primary action visually clear.

Example Theme

MaterialApp(
  theme: ThemeData(
    useMaterial3: true,
    inputDecorationTheme:
        const InputDecorationTheme(
      border: OutlineInputBorder(),
    ),
  ),
  home: const LoginScreen(),
)

32. Authentication UI with Card Design

Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      maxWidth: 500,
    ),
    child: Card(
      margin: const EdgeInsets.all(20),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              const Text(
                'Login',
                style: TextStyle(
                  fontSize: 28,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 24),
              // Email
              // Password
              // Login Button
            ],
          ),
        ),
      ),
    ),
  ),
)

33. Adding a Divider Between Login Methods

If an application supports multiple authentication methods, a divider can visually separate them.

Row(
  children: [
    const Expanded(
      child: Divider(),
    ),
    const Padding(
      padding: EdgeInsets.symmetric(
        horizontal: 12,
      ),
      child: Text('OR'),
    ),
    const Expanded(
      child: Divider(),
    ),
  ],
)

34. Social Login UI

A practical interface may include buttons for external authentication providers when supported by the application's backend.

OutlinedButton.icon(
  onPressed: () {
    print('Continue with provider');
  },
  icon: const Icon(Icons.login),
  label: const Text(
    'Continue with Provider',
  ),
)

The UI button itself does not perform authentication. A suitable authentication SDK or backend integration must be connected to complete the process.


35. Avoiding Multiple Submissions

Users may accidentally tap the Login or Register button multiple times. A loading state can prevent duplicate requests.

if (isLoading) {
  return;
}

setState(() {
  isLoading = true;
});

try {
  await authenticate();
} finally {
  if (mounted) {
    setState(() {
      isLoading = false;
    });
  }
}

36. Client-Side vs Server-Side Validation

Client-Side Validation Server-Side Validation
Runs inside the Flutter application. Runs on the backend/server.
Provides immediate user feedback. Protects backend data and business rules.
Checks required fields and formats. Checks authentication and authorization.
Improves user experience. Provides the security boundary for backend operations.
Can be bypassed by a modified client. Must independently validate untrusted input.

Client-side validation should therefore be treated as a user-experience feature, not as the only security layer.


37. Authentication Data Model Example

class UserModel {
  final String id;
  final String name;
  final String email;
  final String? phone;

  UserModel({
    required this.id,
    required this.name,
    required this.email,
    this.phone,
  });

  factory UserModel.fromJson(
    Map json,
  ) {
    return UserModel(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
      phone: json['phone'] as String?,
    );
  }
}

38. Complete Authentication Flow

                    Application
                         |
                         v
                    Login Screen
                    /          \
                   /            \
          Existing User       New User
                |                  |
                v                  v
             Login            Registration
                |                  |
                v                  v
           Validation          Validation
                |                  |
                └────────┬─────────┘
                         v
                 Authentication API
                         |
                  ┌──────┴──────┐
                  |             |
               Success        Failure
                  |             |
                  v             v
             Home Screen    Error Message

39. Common Authentication UI Errors

  • Not validating the form before submitting.
  • Not handling empty input fields.
  • Using unclear validation messages.
  • Not hiding password characters.
  • Not providing password visibility controls where appropriate.
  • Not displaying a loading state.
  • Allowing multiple authentication requests.
  • Forgetting to dispose controllers.
  • Creating overly wide forms on larger screens.
  • Not supporting scrolling on small screens.
  • Mixing API logic directly throughout UI widgets.
  • Storing passwords insecurely.
  • Relying only on client-side validation for security.

40. Best Practices for a Practical Authentication Interface

  • Keep Login and Registration screens simple and focused.
  • Use Form and TextFormField for structured validation.
  • Use reusable validators.
  • Use reusable authentication widgets.
  • Dispose TextEditingController instances properly.
  • Use appropriate keyboard types.
  • Provide clear validation messages.
  • Use loading indicators during asynchronous requests.
  • Prevent duplicate submissions.
  • Handle backend errors gracefully.
  • Use secure authentication/session mechanisms.
  • Do not store plaintext passwords.
  • Keep authentication services separate from presentation code.
  • Make the interface responsive.
  • Test the interface on different screen sizes.
  • Test invalid, empty, and valid inputs.

41. Testing Checklist

Test Expected Result
Empty email Email validation error appears.
Invalid email Email format error appears.
Empty password Password validation error appears.
Short password Password length error appears.
Password visibility Password switches between hidden and visible states.
Invalid credentials Authentication error is displayed.
Valid credentials User proceeds to the authenticated screen.
Registration with mismatched passwords Password mismatch error appears.
Terms not accepted Registration is prevented.
Repeated button taps Duplicate requests are prevented.
Small screen Form remains usable without overflow.
Keyboard opened User can scroll through the form.

42. Practical Mini Project

Build a complete Flutter Authentication App with the following screens:

  1. Login Screen
  2. Registration Screen
  3. Forgot Password Screen
  4. OTP/Verification Screen
  5. Home Screen

Login Features

  • Email validation
  • Password validation
  • Password visibility
  • Remember Me
  • Forgot Password
  • Loading state
  • Authentication error handling
  • Registration navigation

Registration Features

  • Name validation
  • Email validation
  • Phone validation
  • Password validation
  • Confirm password validation
  • Terms acceptance
  • Loading state
  • Successful registration feedback
  • Login navigation

Advanced Features

  • Backend authentication
  • Session management
  • Email verification
  • Password reset
  • OTP verification
  • Social authentication
  • Protected routes

43. Learning Objectives

  • Understand how authentication interfaces are structured.
  • Create professional Login and Registration screens.
  • Use Flutter forms for authentication input.
  • Validate user input.
  • Manage text input using controllers.
  • Implement password visibility.
  • Implement loading and error states.
  • Navigate between authentication screens.
  • Create reusable authentication widgets.
  • Separate UI and authentication service logic.
  • Prepare the UI for backend integration.
  • Build responsive authentication screens.

44. Official Flutter Resources


45. Flutter Training Resources

For structured Flutter training and course information, explore the following resources:


46. Quick Revision

Concept Purpose
Form Groups and validates authentication fields.
GlobalKey Provides access to the form state.
TextFormField Creates validated input fields.
validator Checks whether entered input is valid.
TextEditingController Retrieves and controls user-entered text.
obscureText Hides password characters.
AutovalidateMode Controls automatic validation behavior.
isLoading Tracks an active authentication request.
Navigator Moves between authentication screens.
AuthService Separates authentication operations from UI code.

47. Key Takeaways

  • A practical Flutter authentication interface should contain clear Login and Registration flows.
  • Form and TextFormField provide a structured way to collect and validate user input.
  • Use GlobalKey to validate the complete form before submission.
  • Use TextEditingController when the application needs to read or control field values.
  • Use password visibility controls to improve usability.
  • Use loading states to communicate asynchronous authentication operations.
  • Use clear error messages for both local validation and backend failures.
  • Separate authentication services from UI code as the application grows.
  • Keep authentication screens responsive and scrollable.
  • Client-side validation improves usability, but server-side validation and secure authentication remain essential for production applications.
Previous Next
whatsapp