Building a Flutter Student Marksheet App with Hive and BLoC (Full Tutorial)
Introduction
In today’s tutorial, we’ll build a complete Student Marksheet App in Flutter using the BLoC pattern for state management and Hive for offline data storage. This app will allow users to:
- Add students and their subject marks
- Update existing marks
- Delete students
- Persist all data even after the app restarts
By the end of this tutorial, you’ll have a powerful offline-capable Flutter app that uses clean architecture and best practices.
Let’s dive in!
Project Overview
Main Features:
- Manage student records
- Calculate total, percentage, and pass/fail status
- Offline storage with Hive
- State management with BLoC
Project Structure:
/lib
/bloc
student_bloc.dart
student_event.dart
student_state.dart
/models
student.dart
/screens
student_page.dart
main.dart
Step 1: Setup the Flutter Project
First, create a new Flutter project:
flutter create student_marksheet
Navigate into the project directory:
cd student_marksheet
Step 2: Add Dependencies
Open your pubspec.yaml file and add these packages:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_bloc: ^9.1.1
hive: ^2.2.3
hive_flutter: ^1.1.0
Then install them:
flutter pub get
Step 3: Initialize Hive in main.dart
We need to initialize Hive and open a box before using it.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:student_marksheet/bloc/student_event.dart';
import 'bloc/student_bloc.dart';
import 'screens/student_page.dart';
import 'models/student.dart';
void main() async {
await Hive.initFlutter();
Hive.registerAdapter(StudentAdapter());
await Hive.openBox<Student>('students');
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Student Marksheet',
home: BlocProvider(
create: (context) => StudentBloc()..add(LoadStudentsEvent()),
child: StudentPage(),
),
);
}
}
Step 4: Create the Student Model
Let’s define a simple model for our students.
// lib/models/student.dart
import 'package:hive/hive.dart';
part 'student.g.dart';
@HiveType(typeId: 0)
class Student extends HiveObject {
@HiveField(0)
String name;
@HiveField(1)
int mark1;
@HiveField(2)
int mark2;
@HiveField(3)
int mark3;
Student({required this.name, required this.mark1, required this.mark2, required this.mark3});
int get total => mark1 + mark2 + mark3;
double get percentage => total / 3;
String get grade {
if (percentage >= 80) return 'A';
if (percentage >= 60) return 'B';
if (percentage >= 40) return 'C';
return 'F';
}
bool get isPass => percentage >= 40;
}
student.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'student.dart';
class StudentAdapter extends TypeAdapter<Student> {
@override
final int typeId = 0;
@override
Student read(BinaryReader reader) {
return Student(
name: reader.readString(),
mark1: reader.readInt(),
mark2: reader.readInt(),
mark3: reader.readInt(),
);
}
@override
void write(BinaryWriter writer, Student obj) {
writer.writeString(obj.name);
writer.writeInt(obj.mark1);
writer.writeInt(obj.mark2);
writer.writeInt(obj.mark3);
}
}
Simple and flexible: students have a name and a map of subjects to marks.
Step 5: Create the BLoC Layer
5.1 Events
Define the possible actions in student_event.dart:
abstract class StudentEvent {}
class LoadStudentsEvent extends StudentEvent {}
class AddStudentEvent extends StudentEvent {
final String name;
final int mark1, mark2, mark3;
AddStudentEvent(this.name, this.mark1, this.mark2, this.mark3);
}
class DeleteStudentEvent extends StudentEvent {
final int index;
DeleteStudentEvent(this.index);
}
5.2 States
Define states in student_state.dart:
import '../models/student.dart';
abstract class StudentState {}
class StudentInitial extends StudentState {}
class StudentLoaded extends StudentState {
final List<Student> students;
StudentLoaded(this.students);
}
5.3 BLoC Logic
Now the main logic in student_bloc.dart:
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import '../models/student.dart';
import 'student_event.dart';
import 'student_state.dart';
class StudentBloc extends Bloc<StudentEvent, StudentState> {
final Box<Student> studentBox = Hive.box<Student>('students');
StudentState? previousState; // Store previous state to detect changes
StudentBloc() : super(StudentInitial()) {
on<LoadStudentsEvent>((event, emit) {
emit(StudentLoaded(studentBox.values.toList()));
});
on<AddStudentEvent>((event, emit) async {
// Track previous state before adding a student
previousState = state;
final newStudent = Student(
name: event.name,
mark1: event.mark1,
mark2: event.mark2,
mark3: event.mark3,
);
await studentBox.add(newStudent);
// Emit new state after adding student
emit(StudentLoaded(studentBox.values.toList()));
});
on<DeleteStudentEvent>((event, emit) async {
// Track previous state before deleting a student
previousState = state;
await studentBox.deleteAt(event.index);
// Emit new state after deleting student
emit(StudentLoaded(studentBox.values.toList()));
});
}
// Helper method to detect changes and show success messages
String getSuccessMessage(StudentState currentState) {
if (previousState is StudentLoaded && currentState is StudentLoaded) {
final prevLength = (previousState as StudentLoaded).students.length;
final currentLength = currentState.students.length;
if (currentLength > prevLength) {
return 'Student added successfully';
} else if (currentLength < prevLength) {
return 'Student deleted successfully';
}
}
return '';
}
}
This BLoC handles adding, updating, deleting, saving to Hive, and loading from Hive!
Step 6: Build the UI
Create a UI to interact with students.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/student_bloc.dart';
import '../bloc/student_event.dart';
import '../bloc/student_state.dart';
import '../models/student.dart';
class StudentPage extends StatelessWidget {
final nameController = TextEditingController();
final mark1Controller = TextEditingController();
final mark2Controller = TextEditingController();
final mark3Controller = TextEditingController();
void _showMessage(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Student Marksheet')),
body: BlocListener<StudentBloc, StudentState>(
listenWhen: (previous, current) =>
previous is StudentLoaded && current is StudentLoaded,
listener: (context, state) {
if (state is StudentLoaded) {
final prevState = context.read<StudentBloc>().previousState;
if (prevState is StudentLoaded) {
final prevLength = prevState.students.length;
final currentLength = state.students.length;
if (currentLength > prevLength) {
_showMessage(context, 'Student added successfully');
} else if (currentLength < prevLength) {
_showMessage(context, 'Student deleted successfully');
}
}
}
},
child: BlocBuilder<StudentBloc, StudentState>(
builder: (context, state) {
if (state is StudentLoaded) {
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: state.students.length,
itemBuilder: (context, index) {
final student = state.students[index];
return ListTile(
title: Text(student.name),
subtitle: Text('Total: ${student.total}, Grade: ${student.grade}, ${student.isPass ? 'Pass' : 'Fail'}'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
BlocProvider.of<StudentBloc>(context).add(DeleteStudentEvent(index));
},
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
TextField(controller: nameController, decoration: InputDecoration(labelText: 'Name')),
TextField(controller: mark1Controller, decoration: InputDecoration(labelText: 'Mark 1'), keyboardType: TextInputType.number),
TextField(controller: mark2Controller, decoration: InputDecoration(labelText: 'Mark 2'), keyboardType: TextInputType.number),
TextField(controller: mark3Controller, decoration: InputDecoration(labelText: 'Mark 3'), keyboardType: TextInputType.number),
ElevatedButton(
onPressed: () {
final name = nameController.text.trim();
final mark1 = int.tryParse(mark1Controller.text.trim());
final mark2 = int.tryParse(mark2Controller.text.trim());
final mark3 = int.tryParse(mark3Controller.text.trim());
if (name.isEmpty) {
_showMessage(context, 'Please enter a name');
return;
}
if (mark1 == null || mark2 == null || mark3 == null) {
_showMessage(context, 'Please enter valid numeric marks');
return;
}
BlocProvider.of<StudentBloc>(context).add(
AddStudentEvent(name, mark1, mark2, mark3),
);
nameController.clear();
mark1Controller.clear();
mark2Controller.clear();
mark3Controller.clear();
},
child: Text('Add Student'),
),
],
),
),
],
);
}
return Center(child: CircularProgressIndicator());
},
),
),
);
}
}
Conclusion
Congratulations! 🎉
You have now built a complete Student Marksheet App in Flutter using Hive and BLoC. The app is:
- Fully offline capable ✅
- Using clean BLoC pattern ✅
- Easy to extend ✅
