Flutter Register Login
Block Pattern

Flutter BLoC Login Tutorial: Full Authentication Flow with BLoC Pattern

πŸ“˜ Flutter BLoC Login / Authentication Example

Authentication is one of the most crucial components in any application. With Flutter, developers can build robust and scalable authentication flows using the BLoC (Business Logic Component) pattern. It separates UI from logic, ensuring maintainability and clarity in code. In this guide, we’ll build a complete Login and Registration System using Flutter and BLoC, backed by a fake JSON REST API powered by json-server.

Instead of Firebase or other cloud services, we simulate API calls locally using json-server. This approach is ideal for testing authentication logic without deploying a real backend. The app includes user registration and login with real-time form validation, loading states, success/failure feedback, and navigation post-authentication β€” all handled gracefully using BLoC.

We’ll cover:

  • Folder structure
  • AuthRepository with HTTP requests
  • Events and States for Login and Registration
  • AuthBloc logic
  • Fully functional Login and Register UI screens

This system is perfect for beginners and intermediate developers looking to master Flutter BLoC patterns while building real-world features.

πŸ“ Folder Structure


lib/
β”œβ”€β”€ auth/
β”‚   β”œβ”€β”€ auth_bloc.dart
β”‚   β”œβ”€β”€ auth_event.dart
β”‚   β”œβ”€β”€ auth_state.dart
β”‚   └── auth_repository.dart
β”œβ”€β”€ login/
β”‚   └── login_screen.dart
β”œβ”€β”€ register/
β”‚   └── register_screen.dart
β”œβ”€β”€ main.dart

πŸ” auth_repository.dart

import 'dart:convert';
import 'package:http/http.dart' as http;

class AuthRepository {
  final String baseUrl =
      'http://localhost:3000/users'; // Make sure this is accessible on your device/emulator

  Future<bool> login({required String email, required String password}) async {
    final url = Uri.parse('$baseUrl?email=$email&password=$password');
    print('Login request: $url');
    final response = await http.get(url);
    print('Login response: ${response.statusCode} - ${response.body}');
    return jsonDecode(response.body).isNotEmpty;
  }

  Future<bool> register(
      {required String email, required String password}) async {
    final url = Uri.parse(baseUrl);
    final body = jsonEncode({'email': email, 'password': password});

    print('Register request to: $url');
    print('Register payload: $body');

    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );

    print('Register response status: ${response.statusCode}');
    print('Register response body: ${response.body}');

    return response.statusCode == 201;
  }
}

πŸ“¦ auth_event.dart

abstract class AuthEvent {}

class LoginRequested extends AuthEvent {
  final String email;
  final String password;
  LoginRequested({required this.email, required this.password});
}

class RegisterRequested extends AuthEvent {
  final String email;
  final String password;
  RegisterRequested({required this.email, required this.password});
}

πŸ“¦ auth_state.dart


abstract class AuthState {}

class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState {}
class AuthFailure extends AuthState {
  final String message;
  AuthFailure(this.message);
}

πŸ“¦ auth_bloc.dart


import 'package:flutter_bloc/flutter_bloc.dart';
import 'auth_event.dart';
import 'auth_state.dart';
import 'auth_repository.dart';

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository authRepository;
  AuthBloc(this.authRepository) : super(AuthInitial()) {
    on(_onLoginRequested);
    on(_onRegisterRequested);
  }

  Future _onLoginRequested(LoginRequested event, Emitter emit) async {
    emit(AuthLoading());
    try {
      final success = await authRepository.login(email: event.email, password: event.password);
      emit(success ? AuthSuccess() : AuthFailure("Invalid credentials"));
    } catch (_) {
      emit(AuthFailure("An error occurred"));
    }
  }

  Future _onRegisterRequested(RegisterRequested event, Emitter emit) async {
    emit(AuthLoading());
    try {
      final success = await authRepository.register(email: event.email, password: event.password);
      emit(success ? AuthSuccess() : AuthFailure("Registration failed"));
    } catch (_) {
      emit(AuthFailure("An error occurred"));
    }
  }
}

πŸ§ͺ login_screen.dart


import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../auth/auth_bloc.dart';
import '../auth/auth_event.dart';
import '../auth/auth_state.dart';
import '../register/register_screen.dart';

class LoginScreen extends StatelessWidget {
  final _formKey = GlobalKey();
  String email = '', password = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: BlocListener<AuthBloc, AuthState>(
        listener: (context, state) {
          if (state is AuthFailure) {
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.message)));
          } else if (state is AuthSuccess) {
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Login Successful')));
          }
        },
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Form(
            key: _formKey,
            child: Column(children: [
              TextFormField(
                decoration: InputDecoration(labelText: 'Email'),
                onChanged: (val) => email = val,
                validator: (val) => val!.contains('@') ? null : 'Enter a valid email',
              ),
              TextFormField(
                decoration: InputDecoration(labelText: 'Password'),
                obscureText: true,
                onChanged: (val) => password = val,
                validator: (val) => val!.length >= 6 ? null : 'Minimum 6 characters',
              ),
              const SizedBox(height: 20),
              BlocBuilder<AuthBloc, AuthState>(builder: (context, state) {
                if (state is AuthLoading) return CircularProgressIndicator();
                return ElevatedButton(
                  onPressed: () {
                    if (_formKey.currentState!.validate()) {
                      context.read().add(LoginRequested(email: email, password: password));
                    }
                  },
                  child: Text('Login'),
                );
              }),
              TextButton(
                onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => RegisterScreen())),
                child: Text('No account? Register here'),
              )
            ]),
          ),
        ),
      ),
    );
  }
}

πŸ§ͺ register_screen.dart


import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../auth/auth_bloc.dart';
import '../auth/auth_event.dart';
import '../auth/auth_state.dart';

class RegisterScreen extends StatelessWidget {
  final _formKey = GlobalKey();
  String email = '', password = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Register')),
      body: BlocListener<AuthBloc, AuthState>(
        listener: (context, state) {
          if (state is AuthFailure) {
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.message)));
          } else if (state is AuthSuccess) {
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Registration Successful')));
            Navigator.pop(context);
          }
        },
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Form(
            key: _formKey,
            child: Column(children: [
              TextFormField(
                decoration: InputDecoration(labelText: 'Email'),
                onChanged: (val) => email = val,
                validator: (val) => val!.contains('@') ? null : 'Enter a valid email',
              ),
              TextFormField(
                decoration: InputDecoration(labelText: 'Password'),
                obscureText: true,
                onChanged: (val) => password = val,
                validator: (val) => val!.length >= 6 ? null : 'Minimum 6 characters',
              ),
              const SizedBox(height: 20),
              BlocBuilder<AuthBloc, AuthState>(builder: (context, state) {
                if (state is AuthLoading) return CircularProgressIndicator();
                return ElevatedButton(
                  onPressed: () {
                    if (_formKey.currentState!.validate()) {
                      context.read().add(RegisterRequested(email: email, password: password));
                    }
                  },
                  child: Text('Register'),
                );
              }),
            ]),
          ),
        ),
      ),
    );
  }
}

πŸš€ main.dart


import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'auth/auth_repository.dart';
import 'auth/auth_bloc.dart';
import 'login/login_screen.dart';
import 'register/register_screen.dart'; // import if needed

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

class MyApp extends StatelessWidget {
  final AuthRepository authRepository = AuthRepository();

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => AuthBloc(authRepository),
      child: MaterialApp(
        title: 'Flutter Auth Demo',
        debugShowCheckedModeBanner: false,
        home: LoginScreen(),
        routes: {
          '/register': (context) => RegisterScreen(),
          // add other routes here
        },
      ),
    );
  }
}

πŸ“‚ json-server db.json


{
  "users": []
}

Run this command: json-server --watch db.json --port 3000


βœ… Summary

Layer File Responsibility
Repository auth_repository.dart Business logic (fake login)
Bloc auth_bloc.dart Handles logic for events & state updates
Events/States auth_event.dart, auth_state.dart Structure for communication
UI login_screen.dart, register_screen.dart Form + BLoC listener + validator

Leave a Reply

Your email address will not be published. Required fields are marked *