Monday, January 26, 2026
HomeFlutterHow to create a login page using flutter step by step guide

How to create a login page using flutter step by step guide

let’s build a clean, practical Flutter login page step-by-step. I’ll give you a working example (with full main.dart, login_page.dart, and home_page.dart) plus explanations and tips to extend it for a real backend.


1) Create a new Flutter project

Run in terminal:

flutter create flutter_login_demo
cd flutter_login_demo

No extra packages are required for the basic demo. (If you later want real auth, you’ll add http, flutter_secure_storage, or a state manager like bloc/provider.)


2) Project structure (simple)

lib/
  main.dart
  pages/
    login_page.dart
    home_page.dart

3) Full code — copy into files

lib/main.dart

import 'package:flutter/material.dart';
import 'pages/login_page.dart';
import 'pages/home_page.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Login Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.indigo,
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => const LoginPage(),
        '/home': (context) => const HomePage(),
      },
    );
  }
}

lib/pages/login_page.dart

import 'package:flutter/material.dart';
import 'dart:async';

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

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _loading = false;
  String? _errorMessage;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  // Mock authentication function to simulate a network call
  Future<bool> _fakeAuthenticate(String email, String password) async {
    await Future.delayed(const Duration(seconds: 2)); // simulate network
    // Example: accept only this credential pair
    return email.trim().toLowerCase() == 'test@example.com' && password == 'password';
  }

  void _submit() async {
    setState(() {
      _errorMessage = null;
    });

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

    final email = _emailController.text;
    final password = _passwordController.text;

    setState(() => _loading = true);

    try {
      final ok = await _fakeAuthenticate(email, password);
      if (ok) {
        // Navigate to home and remove login from stack
        if (!mounted) return;
        Navigator.of(context).pushReplacementNamed('/home');
      } else {
        setState(() {
          _errorMessage = 'Invalid email or password.';
        });
      }
    } catch (e) {
      setState(() {
        _errorMessage = 'An error occurred. Try again.';
      });
    } finally {
      if (mounted) {
        setState(() => _loading = false);
      }
    }
  }

  String? _validateEmail(String? value) {
    if (value == null || value.trim().isEmpty) return 'Please enter your email';
    final email = value.trim();
    final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
    if (!emailRegex.hasMatch(email)) return 'Enter a valid email';
    return null;
  }

  String? _validatePassword(String? 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;
  }

  @override
  Widget build(BuildContext context) {
    // Responsive width constraint
    final width = MediaQuery.of(context).size.width;
    final formWidth = width > 600 ? 480.0 : width * 0.9;

    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(vertical: 24),
          child: ConstrainedBox(
            constraints: BoxConstraints(maxWidth: formWidth),
            child: Card(
              elevation: 6,
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
              margin: const EdgeInsets.all(16),
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Form(
                  key: _formKey,
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const FlutterLogo(size: 72),
                      const SizedBox(height: 12),
                      const Text('Welcome back', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
                      const SizedBox(height: 20),

                      if (_errorMessage != null) ...[
                        Container(
                          width: double.infinity,
                          padding: const EdgeInsets.all(12),
                          margin: const EdgeInsets.only(bottom: 12),
                          decoration: BoxDecoration(
                            color: Theme.of(context).colorScheme.error.withOpacity(0.1),
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Text(
                            _errorMessage!,
                            style: TextStyle(color: Theme.of(context).colorScheme.error),
                          ),
                        ),
                      ],

                      TextFormField(
                        controller: _emailController,
                        keyboardType: TextInputType.emailAddress,
                        decoration: const InputDecoration(
                          labelText: 'Email',
                          prefixIcon: Icon(Icons.email),
                          border: OutlineInputBorder(),
                        ),
                        validator: _validateEmail,
                        autofillHints: const [AutofillHints.username, AutofillHints.email],
                      ),
                      const SizedBox(height: 12),
                      TextFormField(
                        controller: _passwordController,
                        obscureText: true,
                        decoration: const InputDecoration(
                          labelText: 'Password',
                          prefixIcon: Icon(Icons.lock),
                          border: OutlineInputBorder(),
                        ),
                        validator: _validatePassword,
                        autofillHints: const [AutofillHints.password],
                      ),
                      const SizedBox(height: 8),

                      Align(
                        alignment: Alignment.centerRight,
                        child: TextButton(
                          onPressed: () {
                            // stub: implement forgot password flow
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('Forgot password tapped (not implemented)')),
                            );
                          },
                          child: const Text('Forgot password?'),
                        ),
                      ),
                      const SizedBox(height: 8),

                      SizedBox(
                        width: double.infinity,
                        height: 48,
                        child: ElevatedButton(
                          onPressed: _loading ? null : _submit,
                          child: _loading
                              ? const SizedBox(
                                  width: 24,
                                  height: 24,
                                  child: CircularProgressIndicator(strokeWidth: 2),
                                )
                              : const Text('Sign in'),
                        ),
                      ),
                      const SizedBox(height: 12),

                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          const Text("Don't have an account?"),
                          TextButton(
                            onPressed: () {
                              // stub: implement registration flow
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('Register tapped (not implemented)')),
                              );
                            },
                            child: const Text('Register'),
                          )
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Notes on the login code

  • Uses Form + validators (email + password length).
  • Uses controllers and dispose() correctly.
  • Displays an inline error message area and a loading spinner in the button.
  • _fakeAuthenticate simulates network latency and accepts only test@example.com / password. Change for real auth.

lib/pages/home_page.dart

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () {
              // navigate back to login (clear stack)
              Navigator.of(context).pushReplacementNamed('/');
            },
          )
        ],
      ),
      body: const Center(
        child: Text('You are signed in!', style: TextStyle(fontSize: 18)),
      ),
    );
  }
}

4) Run the app

flutter run

Open an emulator or physical device. Use credentials:

  • Email: test@example.com
  • Password: password

5) How to connect to a real backend (overview)

  1. Add http package: flutter pub add http
  2. In _submit() replace _fakeAuthenticate with an HTTP POST to your API (send email/password).
  3. On success, store token securely: use flutter_secure_storage or platform secure storage.
  4. Use token in subsequent API calls (add Authorization header).
  5. Implement error handling for network/timeouts and show friendly messages.

6) Improvements & best practices

  • Move auth logic into a separate service class (e.g., AuthService) → easier testing.
  • Use state management (Provider / Riverpod / Bloc) for larger apps.
  • Add form focus traversal and improved accessibility (labels, semantics).
  • Use HTTPS, validate tokens on app start, refresh tokens if needed.
  • Add unit/widget tests: test validation and navigation flows.

7) Quick checklist before production

  • Use secure storage for tokens.
  • Protect API keys and secrets.
  • Validate server responses and show specific error messages.
  • Implement signup / forgot-password flows and email verification.
  • Add rate-limiting/backoff for retries.
RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments