📘 Flutter CRUD with BLoC and PHP/MySQL API
In modern mobile app development, efficient state management and seamless backend integration are essential for creating scalable and maintainable applications. This tutorial walks you through building a complete Student Management System using Flutter with the BLoC (Business Logic Component) pattern for state management and a PHP/MySQL API for backend operations. Whether you’re displaying a student list, adding new entries, editing records, or deleting students—this project covers the entire CRUD (Create, Read, Update, Delete) lifecycle in a clean and structured way.
By adopting the BLoC pattern, we ensure separation of concerns between UI and business logic, making the codebase modular and testable. On the backend, PHP scripts handle API requests while communicating with a MySQL database to perform data operations efficiently.
This hands-on project is ideal for Flutter developers looking to strengthen their understanding of reactive programming with BLoC, while also learning to connect Flutter apps to real-world backend services. Whether you’re building educational apps or internal enterprise tools, this setup offers a solid foundation for scalable cross-platform development.
✅ What You’ll Build
-
View a student list
-
Add a student
-
Edit student info
-
Delete a student
All done cleanly with Flutter BLoC and powered by a PHP backend.
Project Structure:
lib/ │ ├── main.dart │ ├── models/ │ └── student_model.dart │ ├── repositories/ │ └── student_repository.dart │ ├── blocs/ │ ├── student_bloc.dart │ ├── student_event.dart │ └── student_state.dart │ ├── pages/ │ └── student_page.dart
📦 API Endpoints:
Base URL: https://acesoftech.co.in/API/video/phpmysql-student-crud/
Action | URL | Method |
---|---|---|
View All | view.php |
GET |
Insert | insert.php |
POST |
Edit (get) | edit.php |
GET |
Update | update.php |
POST |
Delete | delete.php |
POST |
🛠️ Step-by-Step Full Tutorial
🔹 1. pubspec.yaml
Dependencies
name: bloc_crud description: A new Flutter project. publish_to: 'none' version: 1.0.0+1 environment: sdk: ^3.6.2 dependencies: flutter: sdk: flutter flutter_bloc: ^8.1.3 http: ^0.13.6 equatable: ^2.0.5 cupertino_icons: ^1.0.8 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 flutter: uses-material-design: true
🔹 2. Create Model – student_model.dart
class Student { final String id; final String firstName; final String lastName; final String email; Student({ required this.id, required this.firstName, required this.lastName, required this.email, }); factory Student.fromJson(Map<String, dynamic> json) { return Student( id: json['id'], firstName: json['first_name'], lastName: json['last_name'], email: json['email'], ); } Map<String, String> toJson() => { 'id': id, 'first_name': firstName, 'last_name': lastName, 'email': email, }; }
🔹 3. Repository – student_repository.dart
import 'dart:convert'; import 'package:bloc_crud/models/student_model.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:http/http.dart' as http; class StudentRepository { final String baseUrl = 'https://acesoftech.co.in/API/video/phpmysql-student-crud'; Future<List<Student>> fetchStudents() async { final response = await http.get(Uri.parse('$baseUrl/view.php')); if (kDebugMode) { print('Fetch Students Response: ${response.body}'); } final List data = jsonDecode(response.body)['records']; return data.map((e) => Student.fromJson(e)).toList(); } Future<void> addStudent(Student student) async { final response = await http.post( Uri.parse('$baseUrl/insert.php'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(student.toJson()), ); if (kDebugMode) { print('Add Response: ${response.body}'); } } Future<void> updateStudent(Student student) async { final response = await http.post( Uri.parse('$baseUrl/update.php'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(student.toJson()), ); if (kDebugMode) { print('Update Response: ${response.body}'); } } Future<void> deleteStudent(String id) async { final response = await http.get(Uri.parse('$baseUrl/delete.php?id=$id')); if (kDebugMode) { print('Delete Response: ${response.body}'); } } Future<Student> getStudent(String id) async { final response = await http.get(Uri.parse('$baseUrl/edit.php?id=$id')); if (kDebugMode) { print('Get Student Response: ${response.body}'); } return Student.fromJson(jsonDecode(response.body)); } }
🔹 4. Bloc Files
student_event.dart
import 'package:bloc_crud/models/student_model.dart'; import 'package:equatable/equatable.dart'; abstract class StudentEvent extends Equatable { const StudentEvent(); @override List<Object> get props => []; } class LoadStudents extends StudentEvent {} class AddStudent extends StudentEvent { final Student student; const AddStudent(this.student); @override List<Object> get props => [student]; } class UpdateStudent extends StudentEvent { final Student student; const UpdateStudent(this.student); @override List<Object> get props => [student]; } class DeleteStudent extends StudentEvent { final String id; const DeleteStudent(this.id); @override List<Object> get props => [id]; }
student_state.dart
import 'package:bloc_crud/models/student_model.dart'; import 'package:equatable/equatable.dart'; abstract class StudentState extends Equatable { @override List<Object?> get props => []; } class StudentInitial extends StudentState {} class StudentLoading extends StudentState {} class StudentLoaded extends StudentState { final List<Student> students; StudentLoaded(this.students); @override List<Object?> get props => [students]; } class StudentAdded extends StudentState {} class StudentUpdated extends StudentState {} class StudentDeleted extends StudentState {} class StudentError extends StudentState { final String message; StudentError(this.message); @override List<Object?> get props => [message]; }
student_bloc.dart
import 'package:bloc_crud/repositories/student_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'student_event.dart'; import 'student_state.dart'; class StudentBloc extends Bloc<StudentEvent, StudentState> { final StudentRepository repository; StudentBloc(this.repository) : super(StudentInitial()) { on<LoadStudents>(_onLoad); on<AddStudent>(_onAdd); on<UpdateStudent>(_onUpdate); on<DeleteStudent>(_onDelete); } void _onLoad(LoadStudents event, Emitter<StudentState> emit) async { emit(StudentLoading()); try { final students = await repository.fetchStudents(); emit(StudentLoaded(students)); } catch (e) { emit(StudentError("Failed to load students")); } } void _onAdd(AddStudent event, Emitter<StudentState> emit) async { try { await repository.addStudent(event.student); emit(StudentAdded()); add(LoadStudents()); } catch (e) { emit(StudentError("Failed to add student")); } } void _onUpdate(UpdateStudent event, Emitter<StudentState> emit) async { try { await repository.updateStudent(event.student); emit(StudentUpdated()); add(LoadStudents()); } catch (e) { emit(StudentError("Failed to update student")); } } void _onDelete(DeleteStudent event, Emitter<StudentState> emit) async { try { await repository.deleteStudent(event.id); emit(StudentDeleted()); add(LoadStudents()); } catch (e) { emit(StudentError("Failed to delete student")); } } }
🔹 5. UI – main.dart
import 'package:bloc_crud/blocs/student_bloc.dart'; import 'package:bloc_crud/blocs/student_event.dart'; import 'package:bloc_crud/pages/student_page.dart'; import 'package:bloc_crud/repositories/student_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { final StudentRepository repo = StudentRepository(); @override Widget build(BuildContext context) { return MaterialApp( home: BlocProvider( create: (_) => StudentBloc(repo)..add(LoadStudents()), child: StudentPage(), ), ); } }
🔹 6. UI – student_page.dart
// student_page.dart import 'package:bloc_crud/blocs/student_bloc.dart'; import 'package:bloc_crud/blocs/student_event.dart'; import 'package:bloc_crud/blocs/student_state.dart'; import 'package:bloc_crud/models/student_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class StudentPage extends StatelessWidget { final fnameController = TextEditingController(); final lnameController = TextEditingController(); final emailController = TextEditingController(); StudentPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Student CRUD with BLoC')), body: BlocListener<StudentBloc, StudentState>( listener: (context, state) { if (state is StudentAdded) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Student added successfully')), ); } else if (state is StudentUpdated) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Student updated successfully')), ); } else if (state is StudentDeleted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Student deleted successfully')), ); } else if (state is StudentError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: ${state.message}')), ); } }, child: BlocBuilder<StudentBloc, StudentState>( builder: (context, state) { if (state is StudentLoading) { return const Center(child: CircularProgressIndicator()); } if (state is StudentLoaded) { return ListView.builder( itemCount: state.students.length, itemBuilder: (context, index) { final s = state.students[index]; return ListTile( title: Text('${s.firstName} ${s.lastName}'), subtitle: Text(s.email), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit), onPressed: () { fnameController.text = s.firstName; lnameController.text = s.lastName; emailController.text = s.email; showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Edit Student'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: fnameController, decoration: const InputDecoration( hintText: 'First Name', ), ), TextField( controller: lnameController, decoration: const InputDecoration( hintText: 'Last Name', ), ), TextField( controller: emailController, decoration: const InputDecoration( hintText: 'Email Address', ), ), ], ), actions: [ ElevatedButton( onPressed: () { final updated = Student( id: s.id, firstName: fnameController.text, lastName: lnameController.text, email: emailController.text, ); context .read<StudentBloc>() .add(UpdateStudent(updated)); Navigator.pop(context); }, child: const Text('Save'), ), ], ), ); }, ), IconButton( icon: const Icon(Icons.delete), onPressed: () { context .read<StudentBloc>() .add(DeleteStudent(s.id)); }, ), ], ), ); }, ); } return const Center(child: Text('No students found')); }, ), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () { fnameController.clear(); lnameController.clear(); emailController.clear(); showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Add Student'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: fnameController, decoration: const InputDecoration( hintText: 'First Name', ), ), TextField( controller: lnameController, decoration: const InputDecoration( hintText: 'Last Name', ), ), TextField( controller: emailController, decoration: const InputDecoration( hintText: 'Email Address', ), ), ], ), actions: [ ElevatedButton( onPressed: () { final newStudent = Student( id: '', // Backend should auto-generate the ID firstName: fnameController.text, lastName: lnameController.text, email: emailController.text, ); context.read<StudentBloc>().add(AddStudent(newStudent)); Navigator.pop(context); }, child: const Text('Add'), ), ], ), ); }, ), ); } }