DEV Community

Arslan Yousaf
Arslan Yousaf

Posted on

Building a Beautiful Login Screen in Flutter: A Complete Guide

Hello Flutter developers! Today, we'll create a modern, beautiful login screen with animations, form validation, and proper error handling. This tutorial will show you how to build a professional-looking login interface from scratch.

What We'll Build

  • A clean, modern login interface
  • Form validation
  • Error handling
  • Loading animations
  • Custom button designs
  • Input field decorations

Prerequisites

  • Basic knowledge of Flutter
  • Flutter development environment set up
  • Text editor or IDE

Step 1: Setting Up the Project Structure

First, let's create a clean project structure:

lib/
  ├── screens/
     └── login_screen.dart
  ├── widgets/
     └── custom_button.dart
  └── main.dart
Enter fullscreen mode Exit fullscreen mode

Step 2: Creating the Login Screen

Here's our complete login_screen.dart:

import 'package:flutter/material.dart';

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

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

class _LoginScreenState extends State<LoginScreen> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLoading = false;
  bool _isPasswordVisible = 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.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const SizedBox(height: 40),
              _buildHeader(),
              const SizedBox(height: 40),
              _buildLoginForm(),
              const SizedBox(height: 20),
              _buildLoginButton(),
              const SizedBox(height: 16),
              _buildSignUpLink(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildHeader() {
    return Column(
      children: const [
        Text(
          'Welcome Back!',
          style: TextStyle(
            fontSize: 32,
            fontWeight: FontWeight.bold,
          ),
        ),
        SizedBox(height: 8),
        Text(
          'Sign in to continue',
          style: TextStyle(
            fontSize: 16,
            color: Colors.grey,
          ),
        ),
      ],
    );
  }

  Widget _buildLoginForm() {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          // Email Field
          TextFormField(
            controller: _emailController,
            keyboardType: TextInputType.emailAddress,
            decoration: InputDecoration(
              labelText: 'Email',
              hintText: 'Enter your email',
              prefixIcon: const Icon(Icons.email_outlined),
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
              ),
              enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
                borderSide: const BorderSide(color: Colors.grey, width: 1),
              ),
              focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
                borderSide: const BorderSide(color: Colors.blue, width: 2),
              ),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter your email';
              }
              if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
                  .hasMatch(value)) {
                return 'Please enter a valid email';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          // Password Field
          TextFormField(
            controller: _passwordController,
            obscureText: !_isPasswordVisible,
            decoration: InputDecoration(
              labelText: 'Password',
              hintText: 'Enter your password',
              prefixIcon: const Icon(Icons.lock_outline),
              suffixIcon: IconButton(
                icon: Icon(
                  _isPasswordVisible
                      ? Icons.visibility_off
                      : Icons.visibility,
                ),
                onPressed: () {
                  setState(() {
                    _isPasswordVisible = !_isPasswordVisible;
                  });
                },
              ),
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
              ),
              enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
                borderSide: const BorderSide(color: Colors.grey, width: 1),
              ),
              focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
                borderSide: const BorderSide(color: Colors.blue, width: 2),
              ),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter your password';
              }
              if (value.length < 6) {
                return 'Password must be at least 6 characters';
              }
              return null;
            },
          ),
          const SizedBox(height: 12),
          // Forgot Password Link
          Align(
            alignment: Alignment.centerRight,
            child: TextButton(
              onPressed: () {
                // Handle forgot password
              },
              child: const Text('Forgot Password?'),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildLoginButton() {
    return ElevatedButton(
      onPressed: _isLoading ? null : _handleLogin,
      style: ElevatedButton.styleFrom(
        padding: const EdgeInsets.symmetric(vertical: 16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
      child: _isLoading
          ? const SizedBox(
              height: 20,
              width: 20,
              child: CircularProgressIndicator(
                strokeWidth: 2,
                valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
              ),
            )
          : const Text(
              'Login',
              style: TextStyle(fontSize: 16),
            ),
    );
  }

  Widget _buildSignUpLink() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Text("Don't have an account?"),
        TextButton(
          onPressed: () {
            // Handle sign up navigation
          },
          child: const Text('Sign Up'),
        ),
      ],
    );
  }

  Future<void> _handleLogin() async {
    if (_formKey.currentState!.validate()) {
      setState(() {
        _isLoading = true;
      });

      try {
        // Simulate API call
        await Future.delayed(const Duration(seconds: 2));

        // Handle successful login
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Login successful!'),
              backgroundColor: Colors.green,
            ),
          );
        }
      } catch (e) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Error: ${e.toString()}'),
              backgroundColor: Colors.red,
            ),
          );
        }
      } finally {
        if (mounted) {
          setState(() {
            _isLoading = false;
          });
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Features Explanation

1. Form Validation

We've implemented comprehensive form validation:

  • Email validation using RegExp
  • Password length check
  • Empty field validation

2. Loading State

The login button shows a loading spinner when:

  • User taps login
  • Form validation passes
  • API call is in progress

3. Error Handling

We handle errors gracefully:

  • Show error messages in SnackBar
  • Clear loading state
  • Preserve form data

4. UI/UX Features

  • Password visibility toggle
  • Proper keyboard types
  • Beautiful input decorations
  • Responsive layout
  • Loading animations

How to Use This Code

  1. Create a new Flutter project
  2. Replace your main.dart content:
import 'package:flutter/material.dart';
import 'screens/login_screen.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Login Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        inputDecorationTheme: InputDecorationTheme(
          filled: true,
          fillColor: Colors.grey[50],
        ),
      ),
      home: const LoginScreen(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Customization Options

Colors and Themes

To change the color scheme:

theme: ThemeData(
  primarySwatch: Colors.purple, // Change primary color
  inputDecorationTheme: InputDecorationTheme(
    filled: true,
    fillColor: Colors.grey[50],
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)
Enter fullscreen mode Exit fullscreen mode

Button Styles

To customize the login button:

ElevatedButton.styleFrom(
  primary: Colors.purple, // Background color
  onPrimary: Colors.white, // Text color
  padding: EdgeInsets.symmetric(vertical: 16),
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12),
  ),
)
Enter fullscreen mode Exit fullscreen mode

Best Practices Used

  1. State Management

    • Proper use of StatefulWidget
    • Controller disposal
    • Loading state handling
  2. Form Handling

    • GlobalKey for form
    • Comprehensive validation
    • Proper keyboard types
  3. Error Handling

    • Try-catch blocks
    • User feedback
    • Graceful error recovery
  4. UI/UX

    • Consistent spacing
    • Clear feedback
    • Loading indicators
    • Proper input validation

Common Issues and Solutions

  1. Form not validating?

    • Check if _formKey is properly connected
    • Ensure validators return null for valid input
  2. Loading spinner not showing?

    • Verify setState is called
    • Check if _isLoading is toggled
  3. Keyboard covering input fields?

    • Wrap with SingleChildScrollView
    • Use resizeToAvoidBottomInset

Next Steps

To enhance this login screen, consider adding:

  • Social login buttons
  • Biometric authentication
  • Remember me functionality
  • Custom animations
  • Dark mode support

Conclusion

You now have a professional-looking login screen with proper validation, error handling, and user feedback. This code provides a solid foundation that you can customize and build upon for your specific needs.


Found this helpful? Follow me for more Flutter tutorials! Feel free to ask questions in the comments.

flutter #dart #ui #mobile #programming #tutorial

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay