Block Pattern

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

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 *