Building a Flutter Student Marksheet App with Hive and BLoC (Full Tutorial)

Building a Flutter Student Marksheet App with Hive and BLoC (Full Tutorial)

AI Reading

Quick summary of this article

This tutorial walks through building a Flutter Student Marksheet App using the BLoC pattern for state management and Hive for offline data storage. The app lets users add, update, and delete student records with subject marks, calculates totals, percentages, and pass/fail status, and persists data across app restarts. It demonstrates a clean, structured approach with separate models, BLoC logic, and UI layers.

  • Core features: Manage student records, calculate total, percentage, grade, and pass/fail status; all data is stored offline using Hive.
  • Tech stack: Uses Flutter, the BLoC pattern for state management, and Hive (with hive_flutter) for local storage.
  • Project structure: Organized into separate folders for BLoC (events, states, logic), models, and screens, following clean architecture principles.
  • Key implementation steps: Initialize Hive in main.dart, define a Student model with Hive annotations, create BLoC events and states, and build a UI with a list view and input form.
  • User feedback: The app shows success messages (e.g., "Student added successfully") by comparing previous and current state lengths after add or delete actions.

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 ✅

 

Leave a Reply

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