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 ✅