A Comprehensive Guide to SQLite CRUD Operations in Flutter with Bloc

Jitesh YadavJitesh Yadav
5 min read

Hello everyone,

Today, I'll guide you through performing CRUD (Create, Read, Update, Delete) operations in Flutter using SQLite, with Bloc for state management. SQLite is a local database that stores data on the device, making it essential to understand SQL queries to utilize its full potential.

First of all, it’s important to know that SQLite is a local database. This means it stores our data locally. To leverage this powerful local database, we should have some knowledge of SQL queries.

Dependencies

The first step is to add the necessary dependencies that will help us along the process.

  1. sqflite

2. flutter_bloc

3. equatble

4. path_provider

Add these dependencies to your pubspec.yaml file under the dependencies section.

dependencies:
flutter:
    sdk: flutter
  sqflite: ^2.3.3+1
  flutter_bloc: ^8.1.5
  equatable: ^2.0.5
  path_provider: ^2.1.3

Now we’ll setup our sqlite file in the lib folder. For this we will first setup a Database Helper class in which we will create a table to perform our operations of CRUD.

my_sqlite.dart

//Dependencies
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
final databaseName = "items.db"; 
 String items = 'CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT)';

// Create and initialize the database
  Future<Database> initDB() async {
    final databasePath = await getDatabasesPath();
    final path = join(databasePath, databaseName);

    return openDatabase(path, version: 1, onCreate: (db, version) async {
      await db.execute(items);
    });
  }
}

Ok, so now that we have created and initialized our database named ‘items’. Now we will implement write the dart functions.

CRUD operations

  1. Create

     Future<void> insertItem(String name) async {
       final db = await initDB();
       await db.insert(
         'items',
         {'name': name},
         conflictAlgorithm: ConflictAlgorithm.replace, // Handle conflicts by replacing existing data
       );
     }
    

    2. Read

     Future<List<Map<String, dynamic>>> fetchItems() async {
       final db = await initDB();
       return await db.query('items');
     }
    

    3. Update

     Future<void> updateItem(int id, String name) async {
       final db = await initDB();
       await db.update(
         'items',
         {'name': name},
         where: 'id = ?',
         whereArgs: [id],
       );
     }
    

    4. Delete

     Future<void> deleteItem(int id) async {
       final db = await initDB();
       await db.delete(
         'items',
         where: 'id = ?',
         whereArgs: [id],
       );
     }
    

    Now, that we have written all the sqlite crud queries in our dart functions, now we will set up our blocs, event and states to manage our state in our application.

    Events

    We’ll define the events for our bloc.

    items_event.dart

     part of 'items_bloc.dart';
    
     sealed class ItemsEvent extends Equatable {
       const ItemsEvent();
    
       @override
       List<Object> get props => [];
     }
    
     //For fetching the items from the database.
     class LoadItems extends ItemsEvent {}
    
     //For adding an item in the database.
     class AddItem extends ItemsEvent {
       final String name;
    
       const AddItem(this.name);
     }
    
     //For updating an existing item in the database. 
     class UpdateItem extends ItemsEvent {
       final int id;
       final String name;
    
       const UpdateItem(this.id, this.name);
     }
    
     //For deleting a particular item in the database.
     class DeleteItem extends ItemsEvent {
       final int id;
    
       const DeleteItem(this.id);
     }
    

    State

    We’ll define the states for our bloc.

    items_state.dart

     part of 'items_bloc.dart';
    
     sealed class ItemsState extends Equatable {
       const ItemsState();
    
       @override
       List<Object> get props => [];
     }
    
     final class ItemsInitial extends ItemsState {}
    
     class ItemLoadInProgress extends ItemsState {}
    
     class ItemLoadSuccess extends ItemsState {
       final List<Map<String, dynamic>> items;
    
       const ItemLoadSuccess(this.items);
     }
    
     class ItemOperationFailure extends ItemsState {
       final String message;
    
       const ItemOperationFailure(this.message);
     }
    

    Bloc

    We’ll define our bloc.

    items_bloc.dart

     //Dependencies:
     import 'package:flutter_bloc/flutter_bloc.dart';
     import 'package:equatable/equatable.dart';
    
     //Paths
     import '../my_sqlite.dart';
    
     part 'items_event.dart';
     part 'items_state.dart';
    
     class ItemsBloc extends Bloc<ItemsEvent, ItemsState> {
       final DatabaseHelper databaseHelper;
       ItemsBloc(this.databaseHelper) : super(ItemsInitial()) {
         on<LoadItems>(_onLoadItems);
         on<AddItem>(_onAddItem);
         on<UpdateItem>(_onUpdateItem);
         on<DeleteItem>(_onDeleteItem);
       }
     // This function is for fetching the items.  
       void _onLoadItems(LoadItems event, Emitter<ItemsState> emit) async {
         emit(ItemLoadInProgress());
         try {
           final items = await databaseHelper.fetchItems();
           emit(ItemLoadSuccess(items));
         } catch (_) {
           emit(const ItemOperationFailure(
               "Failed to fetch the items in the database"));
         }
       }
    
     // This function is for adding items.
       void _onAddItem(AddItem event, Emitter<ItemsState> emit) async {
         await databaseHelper.insertItem(event.name);
         add(LoadItems());
       }
    
     // This function is for updating items. 
       void _onUpdateItem(UpdateItem event, Emitter<ItemsState> emit) async {
         await databaseHelper.updateItem(event.id, event.name);
         add(LoadItems());
       }
    
     //This function is for deleting items with specific id.
       void _onDeleteItem(DeleteItem event, Emitter<ItemsState> emit) async {
         await databaseHelper.deleteItem(event.id);
         add(LoadItems());
       }
     }
    

    Now, that we are done with the bloc also, is the time of UI where we see the payoff of all the buildup that we have been doing.

    main.dart

     import 'package:flutter/material.dart';
     import 'package:flutter_bloc/flutter_bloc.dart';
     import 'bloc/items_bloc.dart';
     import 'home_page.dart';
     import 'my_sqlite.dart';
    
     void main() {
       runApp(const MyApp());
     }
    
     class MyApp extends StatelessWidget {
       const MyApp({super.key});
    
       @override
       Widget build(BuildContext context) {
         return BlocProvider(
           create: (context) => ItemsBloc(DatabaseHelper()),
           child: const MaterialApp(
             title: 'Items',
             home: HomePage(),
           ),
         );
       }
     }
    

    home_page.dart

    ```dart

    import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:items/bloc/items_bloc.dart';

    class HomePage extends StatelessWidget { const HomePage({super.key});

void _showItemDialog(BuildContext context, int? id, String existingName) { final TextEditingController controller = TextEditingController(text: existingName);

showDialog( context: context, builder: (context) { return AlertDialog( title: Text(id == null ? 'Add Item' : 'Update Item'), content: TextField( controller: controller, decoration: const InputDecoration(hintText: 'Enter item name'), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Cancel'), ), TextButton( onPressed: () { final itemName = controller.text; if (id == null) { context.read().add(AddItem(itemName)); } else { context.read().add(UpdateItem(id, itemName)); } Navigator.of(context).pop(); }, child: Text(id == null ? 'Add' : 'Update'), ), ], ); }, ); }

@override Widget build(BuildContext context) { context.read().add(LoadItems()); return Scaffold( appBar: AppBar( title: const Text('Flutter CRUD with Bloc'), ), body: BlocBuilder( builder: (context, state) { if (state is ItemLoadInProgress) { return const Center(child: CircularProgressIndicator()); } else if (state is ItemLoadSuccess) { return ListView.builder( itemCount: state.items.length, itemBuilder: (context, index) { final item = state.items[index]; return Card( color: Colors.white, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(item['name']), Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit), onPressed: () { _showItemDialog( context, item['id'], item['name']); }, ), IconButton( icon: const Icon(Icons.delete), onPressed: () { context .read() .add(DeleteItem(item['id'])); }, ), ], ), ], ), ), ); }, ); } else if (state is ItemOperationFailure) { return const Center(child: Text('Failed to load items')); } return Container(); }, ), floatingActionButton: FloatingActionButton( onPressed: () { _showItemDialog(context, null, ''); }, child: const Icon(Icons.add), ), ); } } ```

In this blog, we covered how to set up SQLite in a Flutter project, create a Database Helper class, and implement CRUD operations. We also set up Bloc to manage our application’s state and integrated it with our UI. This setup ensures a clear separation of concerns, making your app more maintainable and scalable.

You can find all the code in this github link.

Thank you for your time.

0
Subscribe to my newsletter

Read articles from Jitesh Yadav directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jitesh Yadav
Jitesh Yadav