Flutter Library Project
Block Pattern

Part 9 – Flutter BLoC Library Management System – Full Tutorial

Flutter BLoC Library Management System – Full Tutorial

In the world of mobile app development, efficient state management and scalable architecture are key to building maintainable applications. Flutter, Google’s powerful UI toolkit, combined with the BLoC (Business Logic Component) pattern, offers a clean and reactive way to manage app states, especially in complex projects. This tutorial dives into a practical real-world implementation by building a Library Management System using Flutter and BLoC.

The project simulates a full-featured library where users can manage books across different categories. You’ll implement a fake REST API using json-server to simulate backend calls, making this project perfect for local testing without requiring a real server. With Flutter BLoC, we’ll handle all CRUD operations—creating, reading, updating, and deleting books—while keeping the UI responsive and logic separated.

Whether you’re a beginner looking to understand state management or a developer exploring Flutter for scalable apps, this tutorial provides hands-on experience in applying BLoC to real-world scenarios.

🧩 Project Overview

  • Flutter + BLoC pattern for state management
  • Fake JSON REST API using json-server
  • Category-wise Book Management
  • Full CRUD: Add, Update, Delete Books
  • Complete UI built with Flutter widgets

🛠️ Dependencies (pubspec.yaml)


dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.3
  http: ^0.13.5
  equatable: ^2.0.5

🗂️ Project Structure


lib/
├── blocs/
│   └── book_bloc/
│       ├── book_bloc.dart
│       ├── book_event.dart
│       └── book_state.dart
├── models/
│   └── book.dart
├── repositories/
│   └── book_repository.dart
├── screens/
│   └── home_screen.dart
├── widgets/
│   └── book_card.dart
└── main.dart

🧾 Book Model


class Book {
  final int id;
  final String title;
  final String author;
  final String category;

  Book({
    required this.id,
    required this.title,
    required this.author,
    required this.category,
  });

  factory Book.fromJson(Map<String, dynamic> json) {
    int parseId(dynamic idValue) {
      if (idValue == null) return 0;
      if (idValue is int) return idValue;
      if (idValue is String) return int.tryParse(idValue) ?? 0;
      return 0;
    }

    return Book(
      id: parseId(json['id']),
      title: json['title'] ?? '',
      author: json['author'] ?? '',
      category: json['category'] ?? '',
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'author': author,
      'category': category,
    };
  }
}

🌐 Fake JSON API (JSON Server)


npm install -g json-server
json-server --watch db.json --port 3000

Example db.json:


{
  "books": [
    {
      "id": 1,
      "title": "Flutter for Beginners",
      "author": "John Doe",
      "category": "Development"
    }
  ]
}

🧠 BookRepository (HTTP Logic)


import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/book.dart';

class BookRepository {
  final baseUrl = 'http://localhost:3000/books';

  Future<List> fetchBooks() async {
    final response = await http.get(Uri.parse(baseUrl));
    if (response.statusCode == 200) {
      final List data = json.decode(response.body);
      return data.map((e) => Book.fromJson(e)).toList();
    } else {
      throw Exception("Failed to load books");
    }
  }

  Future addBook(Book book) async {
    final Map<String, dynamic> bookMap = {
      'id': book.id.toString(),
      'title': book.title,
      'author': book.author,
      'category': book.category,
    };

    final response = await http.post(
      Uri.parse(baseUrl),
      body: jsonEncode(bookMap),
      headers: {'Content-Type': 'application/json'},
    );

    if (response.statusCode != 201) {
      throw Exception('Failed to add book');
    }
  }

  Future updateBook(Book book) async {
    await http.put(Uri.parse('$baseUrl/${book.id}'),
        headers: {'Content-Type': 'application/json'},
        body: json.encode(book.toJson()));
  }

  Future deleteBook(int id) async {
    final uri = Uri.parse('$baseUrl/$id');
    print('🔴 DELETE Request to: $uri');

    final response = await http.delete(uri);

    print('🔵 Response status: ${response.statusCode}');
    print('🔵 Response body: ${response.body}');

    if (response.statusCode != 200 && response.statusCode != 204) {
      throw Exception('Failed to delete book with id $id');
    }
  }
}

📁 book_event.dart



import 'package:equatable/equatable.dart';
import '../../models/book.dart';

abstract class BookEvent extends Equatable {
  const BookEvent();
  @override
  List<Object?> get props => [];
}

class LoadBooks extends BookEvent {}

class AddBook extends BookEvent {
  final Book book;
  const AddBook(this.book);

  @override
  List<Object?> get props => [book];
}

class UpdateBook extends BookEvent {
  final Book book;
  const UpdateBook(this.book);

  @override
  List<Object?> get props => [book];
}

class DeleteBook extends BookEvent {
  final int id;
  const DeleteBook(this.id);

  @override
  List<Object?> get props => [id];
}

📁 book_state.dart



import 'package:equatable/equatable.dart';
import '../../models/book.dart';

abstract class BookState extends Equatable {
  @override
  List<Object?> get props => [];
}

class BookInitial extends BookState {}

class BookLoading extends BookState {}

class BookLoaded extends BookState {
  final List books;
  BookLoaded(this.books);

  @override
  List<Object?> get props => [books];
}

class BookError extends BookState {
  final String message;
  BookError(this.message);

  @override
  List<Object?> get props => [message];
}

📁 book_bloc.dart



import 'package:flutter_bloc/flutter_bloc.dart';
import 'book_event.dart';
import 'book_state.dart';
import '../../repositories/book_repository.dart';
import '../../models/book.dart';

class BookBloc extends Bloc<BookEvent, BookState> {
  final BookRepository repository;

  BookBloc(this.repository) : super(BookInitial()) {
    on(_onLoadBooks);
    on(_onAddBook);
    on(_onUpdateBook);
    on(_onDeleteBook);
  }

  Future _onLoadBooks(LoadBooks event, Emitter emit) async {
    emit(BookLoading());
    try {
      final books = await repository.fetchBooks();
      emit(BookLoaded(books));
    } catch (_) {
      emit(BookError("Failed to load books"));
    }
  }

  Future _onAddBook(AddBook event, Emitter emit) async {
    await repository.addBook(event.book);
    add(LoadBooks());
  }

  Future _onUpdateBook(UpdateBook event, Emitter emit) async {
    await repository.updateBook(event.book);
    add(LoadBooks());
  }

  Future _onDeleteBook(DeleteBook event, Emitter emit) async {
    await repository.deleteBook(event.id);
    add(LoadBooks());
  }
}

🖥️ main.dart



import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'repositories/book_repository.dart';
import 'blocs/book_bloc/book_bloc.dart';
import 'blocs/book_bloc/book_event.dart';
import 'screens/home_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final BookRepository repository = BookRepository();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Library App',
      home: BlocProvider(
        create: (_) => BookBloc(repository)..add(LoadBooks()),
        child: HomeScreen(),
      ),
    );
  }
}

🖥️ home_screen.dart


import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/book_bloc/book_bloc.dart';
import '../blocs/book_bloc/book_event.dart';
import '../blocs/book_bloc/book_state.dart';
import '../models/book.dart';
import '../widgets/book_card.dart';

class HomeScreen extends StatelessWidget {
  final titleController = TextEditingController();
  final authorController = TextEditingController();
  final categoryController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Library Management")),
      body: BlocBuilder<BookBloc, BookState>(
        builder: (context, state) {
          if (state is BookLoading)
            return Center(child: CircularProgressIndicator());
          if (state is BookLoaded) {
            return ListView(
              children:
                  state.books.map((book) => BookCard(book: book)).toList(),
            );
          }
          return Center(child: Text("Something went wrong"));
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () =>
            showDialog(context: context, builder: (_) => bookDialog(context)),
        child: Icon(Icons.add),
      ),
    );
  }

  Widget bookDialog(BuildContext context, {Book? book}) {
    if (book != null) {
      titleController.text = book.title;
      authorController.text = book.author;
      categoryController.text = book.category;
    }

    return AlertDialog(
      title: Text(book == null ? "Add Book" : "Edit Book"),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
              controller: titleController,
              decoration: InputDecoration(labelText: "Title")),
          TextField(
              controller: authorController,
              decoration: InputDecoration(labelText: "Author")),
          TextField(
              controller: categoryController,
              decoration: InputDecoration(labelText: "Category")),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () {
            final newBook = Book(
              id: book?.id ?? DateTime.now().millisecondsSinceEpoch,
              title: titleController.text,
              author: authorController.text,
              category: categoryController.text,
            );
            context
                .read()
                .add(book == null ? AddBook(newBook) : UpdateBook(newBook));
            Navigator.pop(context);
          },
          child: Text(book == null ? "Add" : "Update"),
        )
      ],
    );
  }
}

✅ Summary Table

Feature Implementation
JSON API json-server
State Management BLoC
UI Flutter Widgets
CRUD Add / Update / Delete
Category View Book by Category

🚀 Coming Next

  • Clean architecture
  • Persisting state with HydratedBloc
  • Authentication using BLoC

Leave a Reply

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