Flutter With Sqflite Database Tutorial

Flutter SQLite CRUD with sqflite: Complete Tutorial (2026) | D.Tech Academic

Flutter SQLite CRUD with sqflite: Complete Tutorial (2026)

Last Updated: | Author: Deep Singh

SQLite is the most widely deployed database in the world — and for good reason. It's fast, it runs entirely on-device, and it needs no server. In Flutter, the sqflite plugin gives you full access to SQLite, so you can store structured data locally without touching a backend.

In this tutorial, you'll build a small student records app that performs the two most common database operations: insert and fetch. We'll cover setting up the database, creating a table, inserting rows, and displaying them in a list. By the end, you'll have a solid foundation for any local persistence feature in Flutter.

Adding the Plugins

We need two plugins: one for the SQLite database itself, and one for finding the right path on the device to store the database file.

Run these commands in your project folder:

flutter pub add sqflite
flutter pub add path_provider
flutter pub add path

Or add them manually to your pubspec.yaml file:

dependencies:
  sqflite: ^2.3.3
  path_provider: ^2.1.4
  path: ^1.9.0

Then run flutter pub get.

Note on versions: The versions above are current as of 2026. Older tutorials may reference sqflite: ^2.0.0+3 and path_provider: ^2.0.1, which still work but miss years of fixes and improvements. Use the latest unless you have a specific reason to pin an older version.

Flutter SQLite student records app interface
The student records app in action

The Database Class (operations.dart)

This file contains all the SQLite logic. It follows the singleton pattern — there's only ever one instance, and one open database connection.

import 'dart:io';

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

import 'main.dart';

class MyDatabase {
    static const String DATABASE_NAME = 'mycollege.db';
    static const String TABLE_NAME = 'students';
    static const String ROLL = 'roll';
    static const String NAME = 'name';
    static const String MARKS = 'marks';
    static const int DATABASE_VERSION = 1;

    MyDatabase._();

    static final MyDatabase instance = MyDatabase._();
    static Database? _database;

    Future<Database> get database async {
        if (_database != null) return _database!;
        _database = await _initDB();
        return _database!;
    }

    Future<Database> _initDB() async {
        final Directory documentsDirectory =
            await getApplicationDocumentsDirectory();
        final String dbPath = join(documentsDirectory.path, DATABASE_NAME);
        return await openDatabase(
            dbPath,
            version: DATABASE_VERSION,
            onCreate: _onCreate,
        );
    }

    Future<void> _onCreate(Database db, int version) async {
        await db.execute('CREATE TABLE $TABLE_NAME ('
            '$ROLL INTEGER PRIMARY KEY, '
            '$NAME TEXT, '
            '$MARKS REAL)');
    }

    Future<int> insert(Student student) async {
        final db = await database;
        return await db.insert(
            TABLE_NAME,
            student.toMap(),
            conflictAlgorithm: ConflictAlgorithm.replace,
        );
    }

    Future<List<Student>> getAllStudents() async {
        final db = await database;
        final List<Map<String, dynamic>> maps = await db.query(TABLE_NAME);
        return maps.map((map) => Student.fromMap(map)).toList();
    }

    Future<Student?> fetchStudentByRoll(int roll) async {
        final db = await database;
        final res = await db.query(
            TABLE_NAME,
            where: '$ROLL = ?',
            whereArgs: [roll],
        );
        if (res.isEmpty) return null;
        return Student.fromMap(res.first);
    }
}

The UI (main.dart)

The UI has three text fields, three buttons, and a list area. Each button does one thing: insert, fetch a single student, or fetch everything.

import 'package:flutter/material.dart';

import 'operations.dart';

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

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

    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData(primarySwatch: Colors.blue),
            home: const MyHomePage(title: 'SQLite Demo'),
        );
    }
}

class MyHomePage extends StatefulWidget {
    const MyHomePage({super.key, required this.title});

    final String title;

    @override
    State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
    final TextEditingController _rollController = TextEditingController();
    final TextEditingController _nameController = TextEditingController();
    final TextEditingController _marksController = TextEditingController();

    final FocusNode _rollFocus = FocusNode();
    final FocusNode _nameFocus = FocusNode();
    final FocusNode _marksFocus = FocusNode();

    Widget _listWidget = const SizedBox.shrink();

    @override
    void dispose() {
        _rollController.dispose();
        _nameController.dispose();
        _marksController.dispose();
        _rollFocus.dispose();
        _nameFocus.dispose();
        _marksFocus.dispose();
        super.dispose();
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: const Text('Form Data')),
            body: SingleChildScrollView(
                padding: const EdgeInsets.all(20),
                child: Column(
                    children: [
                        TextFormField(
                            controller: _rollController,
                            focusNode: _rollFocus,
                            keyboardType: TextInputType.number,
                            decoration: const InputDecoration(
                                labelText: 'Enter Roll',
                                border: OutlineInputBorder(),
                            ),
                        ),
                        const SizedBox(height: 10),
                        TextFormField(
                            controller: _nameController,
                            focusNode: _nameFocus,
                            decoration: const InputDecoration(
                                labelText: 'Enter Name',
                                border: OutlineInputBorder(),
                            ),
                        ),
                        const SizedBox(height: 10),
                        TextFormField(
                            controller: _marksController,
                            focusNode: _marksFocus,
                            keyboardType: TextInputType.number,
                            decoration: const InputDecoration(
                                labelText: 'Enter Marks',
                                border: OutlineInputBorder(),
                            ),
                        ),
                        const SizedBox(height: 10),
                        ButtonBar(
                            alignment: MainAxisAlignment.start,
                            children: [
                                ElevatedButton(
                                    onPressed: _onInsert,
                                    child: const Text('Insert'),
                                ),
                                ElevatedButton(
                                    onPressed: _onFetchByRoll,
                                    child: const Text('Fetch By Roll'),
                                ),
                                ElevatedButton(
                                    onPressed: _onFetchAll,
                                    child: const Text('Fetch All'),
                                ),
                            ],
                        ),
                        _listWidget,
                    ],
                ),
            ),
        );
    }

    Future<void> _onInsert() async {
        if (!_validate()) return;
        final student = Student(
            roll: int.parse(_rollController.text),
            name: _nameController.text.trim(),
            marks: double.parse(_marksController.text),
        );
        final id = await MyDatabase.instance.insert(student);
        debugPrint('Inserted row id: $id');
        _clearFields();
    }

    Future<void> _onFetchByRoll() async {
        if (!_validate()) return;
        final roll = int.parse(_rollController.text);
        final student = await MyDatabase.instance.fetchStudentByRoll(roll);
        if (student != null) {
            debugPrint('Found: ${student.name} (${student.marks})');
        } else {
            debugPrint('No student with roll $roll');
        }
    }

    Future<void> _onFetchAll() async {
        setState(() {
            _listWidget = _buildList();
        });
    }

    Widget _buildList() {
        return FutureBuilder<List<Student>>(
            future: MyDatabase.instance.getAllStudents(),
            builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                    return const Padding(
                        padding: EdgeInsets.all(20),
                        child: CircularProgressIndicator(),
                    );
                }
                if (snapshot.hasError) {
                    return Padding(
                        padding: const EdgeInsets.all(20),
                        child: Text('Error: ${snapshot.error}'),
                    );
                }
                final students = snapshot.data ?? [];
                if (students.isEmpty) {
                    return const Padding(
                        padding: EdgeInsets.all(20),
                        child: Text('No records yet.'),
                    );
                }
                return ListView.builder(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    itemCount: students.length,
                    itemBuilder: (context, index) {
                        final s = students[index];
                        return ListTile(
                            leading: Text(s.roll.toString()),
                            title: Center(child: Text(s.name)),
                            trailing: Text(s.marks.toString()),
                        );
                    },
                );
            },
        );
    }

    bool _validate() {
        if (_rollController.text.trim().isEmpty) {
            _showSnack('Please enter roll');
            _rollFocus.requestFocus();
            return false;
        }
        if (_nameController.text.trim().isEmpty) {
            _showSnack('Please enter name');
            _nameFocus.requestFocus();
            return false;
        }
        if (_marksController.text.trim().isEmpty) {
            _showSnack('Please enter marks');
            _marksFocus.requestFocus();
            return false;
        }
        return true;
    }

    void _showSnack(String message) {
        ScaffoldMessenger.of(context)
            .showSnackBar(SnackBar(content: Text(message)));
    }

    void _clearFields() {
        _rollController.clear();
        _nameController.clear();
        _marksController.clear();
    }
}

class Student {
    final int roll;
    final String name;
    final double marks;

    const Student({required this.roll, required this.name, required this.marks});

    factory Student.fromMap(Map<String, dynamic> map) {
        return Student(
            roll: map['roll'] as int,
            name: map['name'] as String,
            marks: (map['marks'] as num).toDouble(),
        );
    }

    Map<String, dynamic> toMap() {
        return {
            'roll': roll,
            'name': name,
            'marks': marks,
        };
    }
}

How the Code Works

Let's walk through the important parts.

1. The Singleton Database Class

MyDatabase has a private constructor (MyDatabase._()) and a single static instance. This ensures there's only ever one connection to the database — opening it multiple times would cause conflicts and waste resources.

2. Lazy Initialization

Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDB();
    return _database!;
}

The database is only opened the first time someone asks for it. Subsequent calls return the cached connection. This is important for performance — opening a SQLite database is not free.

3. Finding the Right Path

final Directory documentsDirectory =
    await getApplicationDocumentsDirectory();
final String dbPath = join(documentsDirectory.path, DATABASE_NAME);

path_provider gives you a platform-appropriate directory where the app can store persistent files. Then path.join() builds a platform-correct path to the database file. The path package handles the slash direction for you — don't concatenate strings manually.

4. Creating the Table

await db.execute('CREATE TABLE $TABLE_NAME ('
    '$ROLL INTEGER PRIMARY KEY, '
    '$NAME TEXT, '
    '$MARKS REAL)');

onCreate is called only the first time the database is created. Use INTEGER PRIMARY KEY for the roll number — this enforces uniqueness automatically. TEXT is for strings, REAL for floating-point numbers.

5. Inserting

conflictAlgorithm: ConflictAlgorithm.replace means: if a row with the same primary key already exists, replace it instead of throwing an error. This effectively makes insert behave like an upsert.

6. Fetching All

db.query(TABLE_NAME) returns a List<Map<String, dynamic>> — one map per row. We map each one through Student.fromMap to get typed objects, which is much safer to work with than raw maps.

7. Fetching One

final res = await db.query(
    TABLE_NAME,
    where: '$ROLL = ?',
    whereArgs: [roll],
);

The ? placeholder plus whereArgs is the safe way to pass values into a SQL query. Never build the query by string concatenation — that opens the door to SQL injection.

8. The FutureBuilder for the List

When the "Fetch All" button is tapped, we rebuild the widget tree and let a FutureBuilder handle the async database query. It shows a spinner while loading, an error message if something goes wrong, an empty state if no records exist, and the list itself when data is available.

Common Pitfalls

  • Don't call getApplicationDocumentsDirectory() from a widget's build method. It's an async call — put it in initState or, better, inside the database class as shown.
  • Don't open the database more than once. If you call openDatabase() in multiple places without caching, you'll end up with file lock errors.
  • Remember to dispose of controllers and focus nodes. Failing to do so leaks memory, especially if the widget is rebuilt many times.
  • Don't return raw maps from the database layer. Always convert to typed objects. It catches schema errors at the boundary instead of somewhere deep in the UI.
  • Use transactions for bulk inserts. If you're inserting hundreds of rows, wrap them in a transaction — SQLite will be dramatically faster.
  • Handle migration. This tutorial uses version: 1 with no onUpgrade. When you change the schema later, you'll need to provide one, or you'll crash on existing installs.
  • Don't use SQLite on the web. sqflite doesn't work in browsers. For Flutter web, use sqflite_common_ffi_web or a different storage solution entirely.

Wrapping Up

You now have a working SQLite setup in Flutter — insert, fetch one, and fetch all. The same pattern extends naturally to update, delete, and complex queries. Once you're comfortable with the basics here, the next steps are:

  • Add update and delete methods — db.update() and db.delete() follow the same shape as insert.
  • Add migration logic — implement onUpgrade so schema changes don't break existing users.
  • Use transactions for bulk operations. They're a huge speed win.
  • Consider Drift or Moor if you want compile-time safe queries instead of raw SQL strings.

For anything more than simple key-value or list storage, SQLite is usually a better choice than shared_preferences. It's fast, reliable, and works entirely offline — a perfect fit for mobile.

You might also find these helpful:

No comments: