Let’s go step-by-step through how to implement Login and Signup pages using BLoC in Flutter — following Clean Architecture principles (data → domain → presentation) and explaining each part clearly.
🧱 Project Overview
We’ll build:
- Splash → Login → Signup → Home
- Using BLoC state management
- With DummyJSON API
- Login API →
https://dummyjson.com/auth/login - Signup API →
https://dummyjson.com/users/add
- Login API →
🗂 Folder Structure (Clean Architecture + BLoC)
lib/
├── core/
│ └── constants/
│ └── api_constants.dart
│
├── data/
│ ├── models/
│ │ ├── user_model.dart
│ └── repositories/
│ └── auth_repository.dart
│
├── domain/
│ └── entities/
│ └── user_entity.dart
│
├── presentation/
│ ├── blocs/
│ │ ├── login/
│ │ │ ├── login_bloc.dart
│ │ │ ├── login_event.dart
│ │ │ └── login_state.dart
│ │ ├── signup/
│ │ │ ├── signup_bloc.dart
│ │ │ ├── signup_event.dart
│ │ │ └── signup_state.dart
│ ├── screens/
│ │ ├── login_screen.dart
│ │ ├── signup_screen.dart
│ │ └── home_screen.dart
│ └── widgets/
│ └── custom_text_field.dart
│
└── main.dart
Step 1️⃣ — Define API Constants
lib/core/constants/api_constants.dart
class ApiConstants {
static const String baseUrl = "https://dummyjson.com";
static const String login = "$baseUrl/auth/login";
static const String signup = "$baseUrl/users/add";
}
Step 2️⃣ — Create User Model & Entity
lib/domain/entities/user_entity.dart
class UserEntity {
final int id;
final String username;
final String email;
final String token;
UserEntity({
required this.id,
required this.username,
required this.email,
required this.token,
});
}
lib/data/models/user_model.dart
import '../../domain/entities/user_entity.dart';
class UserModel extends UserEntity {
UserModel({
required super.id,
required super.username,
required super.email,
required super.token,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] ?? 0,
username: json['username'] ?? '',
email: json['email'] ?? '',
token: json['token'] ?? '',
);
}
}
Step 3️⃣ — Create Auth Repository
lib/data/repositories/auth_repository.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../core/constants/api_constants.dart';
import '../models/user_model.dart';
class AuthRepository {
Future<UserModel> login(String username, String password) async {
final response = await http.post(
Uri.parse(ApiConstants.login),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'username': username, 'password': password}),
);
if (response.statusCode == 200) {
return UserModel.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to login: ${response.body}');
}
}
Future<UserModel> signup(String username, String email, String password) async {
final response = await http.post(
Uri.parse(ApiConstants.signup),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'username': username,
'email': email,
'password': password,
}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return UserModel.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to signup: ${response.body}');
}
}
}
Step 4️⃣ — Create BLoC for Login
login_event.dart
import 'package:equatable/equatable.dart';
abstract class LoginEvent extends Equatable {
@override
List<Object> get props => [];
}
class LoginButtonPressed extends LoginEvent {
final String username;
final String password;
LoginButtonPressed(this.username, this.password);
}
login_state.dart
import 'package:equatable/equatable.dart';
import '../../../domain/entities/user_entity.dart';
abstract class LoginState extends Equatable {
@override
List<Object?> get props => [];
}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {
final UserEntity user;
LoginSuccess(this.user);
}
class LoginFailure extends LoginState {
final String message;
LoginFailure(this.message);
}
login_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../data/repositories/auth_repository.dart';
import '../../../domain/entities/user_entity.dart';
import 'login_event.dart';
import 'login_state.dart';
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthRepository authRepository;
LoginBloc(this.authRepository) : super(LoginInitial()) {
on<LoginButtonPressed>((event, emit) async {
emit(LoginLoading());
try {
final user = await authRepository.login(event.username, event.password);
emit(LoginSuccess(user));
} catch (e) {
emit(LoginFailure(e.toString()));
}
});
}
}
Step 5️⃣ — Create BLoC for Signup
signup_event.dart
import 'package:equatable/equatable.dart';
abstract class SignupEvent extends Equatable {
@override
List<Object> get props => [];
}
class SignupButtonPressed extends SignupEvent {
final String username;
final String email;
final String password;
SignupButtonPressed(this.username, this.email, this.password);
}
signup_state.dart
import 'package:equatable/equatable.dart';
import '../../../domain/entities/user_entity.dart';
abstract class SignupState extends Equatable {
@override
List<Object?> get props => [];
}
class SignupInitial extends SignupState {}
class SignupLoading extends SignupState {}
class SignupSuccess extends SignupState {
final UserEntity user;
SignupSuccess(this.user);
}
class SignupFailure extends SignupState {
final String message;
SignupFailure(this.message);
}
signup_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../data/repositories/auth_repository.dart';
import '../../../domain/entities/user_entity.dart';
import 'signup_event.dart';
import 'signup_state.dart';
class SignupBloc extends Bloc<SignupEvent, SignupState> {
final AuthRepository authRepository;
SignupBloc(this.authRepository) : super(SignupInitial()) {
on<SignupButtonPressed>((event, emit) async {
emit(SignupLoading());
try {
final user = await authRepository.signup(event.username, event.email, event.password);
emit(SignupSuccess(user));
} catch (e) {
emit(SignupFailure(e.toString()));
}
});
}
}
Step 6️⃣ — Create Login & Signup Screens
login_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/login/login_bloc.dart';
import '../blocs/login/login_event.dart';
import '../blocs/login/login_state.dart';
import 'home_screen.dart';
import 'signup_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _username = TextEditingController();
final _password = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<LoginBloc, LoginState>(
listener: (context, state) {
if (state is LoginSuccess) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
} else if (state is LoginFailure) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
}
},
builder: (context, state) {
if (state is LoginLoading) {
return const Center(child: CircularProgressIndicator());
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(controller: _username, decoration: const InputDecoration(labelText: 'Username')),
TextField(controller: _password, decoration: const InputDecoration(labelText: 'Password'), obscureText: true),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
context.read<LoginBloc>().add(
LoginButtonPressed(_username.text, _password.text),
);
},
child: const Text('Login'),
),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const SignupScreen()));
},
child: const Text("Don't have an account? Sign up"),
),
],
),
);
},
),
);
}
}
signup_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/signup/signup_bloc.dart';
import '../blocs/signup/signup_event.dart';
import '../blocs/signup/signup_state.dart';
import 'home_screen.dart';
class SignupScreen extends StatefulWidget {
const SignupScreen({super.key});
@override
State<SignupScreen> createState() => _SignupScreenState();
}
class _SignupScreenState extends State<SignupScreen> {
final _username = TextEditingController();
final _email = TextEditingController();
final _password = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Signup')),
body: BlocConsumer<SignupBloc, SignupState>(
listener: (context, state) {
if (state is SignupSuccess) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
} else if (state is SignupFailure) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
}
},
builder: (context, state) {
if (state is SignupLoading) {
return const Center(child: CircularProgressIndicator());
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(controller: _username, decoration: const InputDecoration(labelText: 'Username')),
TextField(controller: _email, decoration: const InputDecoration(labelText: 'Email')),
TextField(controller: _password, decoration: const InputDecoration(labelText: 'Password'), obscureText: true),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
context.read<SignupBloc>().add(
SignupButtonPressed(_username.text, _email.text, _password.text),
);
},
child: const Text('Signup'),
),
],
),
);
},
),
);
}
}
Step 7️⃣ — Home Screen
home_screen.dart
import 'package:flutter/material.dart';
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 Home Screen!')),
);
}
}
Step 8️⃣ — Initialize BLoC in main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'data/repositories/auth_repository.dart';
import 'presentation/blocs/login/login_bloc.dart';
import 'presentation/blocs/signup/signup_bloc.dart';
import 'presentation/screens/login_screen.dart';
void main() {
final authRepository = AuthRepository();
runApp(MyApp(authRepository: authRepository));
}
class MyApp extends StatelessWidget {
final AuthRepository authRepository;
const MyApp({super.key, required this.authRepository});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => LoginBloc(authRepository)),
BlocProvider(create: (_) => SignupBloc(authRepository)),
],
child: MaterialApp(
title: 'Flutter BLoC Auth',
theme: ThemeData(primarySwatch: Colors.blue),
home: const LoginScreen(),
),
);
}
}
✅ Output Flow
- Login Page
→ Enter username (kminchelle) and password (0lelplR)
→ On success → Goes to Home Page - Signup Page
→ Fill fields
→ On success → Goes to Home Page
🚀 Key Concepts Recap
| Layer | Role | Example |
|---|---|---|
| Data | Handles API & JSON | AuthRepository |
| Domain | Defines business entities | UserEntity |
| Presentation | UI + BLoC | LoginScreen, SignupBloc |
| State Management | Handles UI states | BLoC (loading, success, failure) |
1 comment
[…] How to implement Login and Signup pages using Bloc in Flutter […]