Flutter Drift Database : Complete Guide for Local Persistence



The Ultimate Guide to Flutter Drift Database

Why local storage is important in Flutter apps ? have you any time faced issue in your app storing data within app if yes this blog will help you in providing much better solution than sqlite using Sqflite plugin in flutter.

Note: This guide is the 2026 updated version of our original Flutter Drift implementation. We have refined the code for Flutter 3.x and added advanced CRUD operations and troubleshooting steps.

Why Choose Drift for Flutter Offline Storage?

Why Drift ? (previously Moor db), Drift is a wrapper/ORM (Object Relational Mapper) built on top of SQLite. It makes database handling easier, safer, and more Flutter-friendly.

Setting Up Drift in Flutter: Step-by-Step

Time needed: 15 minutes

A step-by-step guide to integrating the Drift (formerly Moor) reactive persistence library into your Flutter application for type-safe SQLite storage.

  1. Step 1: Add Dependencies to pubspec.yaml

    Open your pubspec.yaml file and add drift and sqlite3_flutter_libs under dependencies. Add drift_dev and build_runner under dev_dependencies to enable code generation.
    Code Tip: Ensure you run flutter pub get after saving.

  2. Step 2: Define Your Database Table

    Description: Create a new Dart file (e.g., database.dart) and define your table schema using Drift’s DSL. Use the Table class to specify columns like IntColumn or TextColumn.
    Action: Annotate your database class with @DriftDatabase(tables: [YourTable]).

  3. Step 3: Generate the Code with Build Runner

    Description: Run the generator command in your terminal to create the .g.dart file which contains the boilerplate code and query logic.
    Command: flutter pub run build_runner build

  4. Step 4: Initialize the Database Class

    Description: Create a constructor for your database class that opens the connection using NativeDatabase for mobile or WasmDatabase for web.
    Tip: Use a singleton pattern or a Provider to access the database instance across your Flutter app.

  5. Step 5: Perform CRUD Operations

    Description: Use the generated getters and methods to insert, update, or stream data from your tables. Drift provides reactive streams that automatically update your UI when the data changes.

Importance of Drift over SQLite in Flutter

1. Type Safety
  • Drift generates Dart classes for your tables.
  • Queries return strongly-typed objects, so you catch errors at compile time instead of runtime. (e.g., If a column expects an int, Drift won’t let you insert a String)
2. Less Boilerplate, More Productivity
  • No need to write SQL manually for CRUD operations.
  • Drift automatically generates query methods using build_runner.
3. Reactive Streams
  • Drift integrates seamlessly with StreamBuilder/FutureBuilder in Flutter.
  • Whenever data in the database changes, your UI updates automatically. (This isn’t natively possible with plain SQLite.)
4. Schema Management & Migrations
  • With SQLite (via sqflite), you write migrations manually.
  • Drift handles schema changes more gracefully with built-in support.
5. Cross-Platform Support
  • Drift works not only with Flutter mobile apps but also on desktop (Windows, macOS, Linux) and web.
  • Sqflite works only on Android/iOS.
6. Custom Queries & Joins Made Easy
  • Drift allows both high-level Dart-based queries and raw SQL if needed.
  • You can easily write joins, filters, and aggregations without complex SQL syntax.

Performing CRUD Operations in Drift

Once your database is initialized and the code generation is complete, you can begin managing your data. Drift provides a fluent Dart API that is both type-safe and reactive.

Create: Inserting Data

To insert data, Drift uses Companions. Companions are generated classes that allow you to specify only the columns you want to set, letting the database handle default values or auto-increments for the rest.

// Insert a single entry
Future<int> addUser(UsersCompanion entry) {
  return into(users).insert(entry);
}

// Usage example
await database.addUser(
  UsersCompanion.insert(
    userName: 'AmplifyAbhi',
    userAge: const Value(25), // Use Value() for optional/nullable fields
  ),
);

Read: Fetching Data (Futures vs. Streams)

One of Drift’s most powerful features is Reactive Streams.

  • get(): Returns a Future<List<User>>. Use this for one-time data fetching.
  • watch(): Returns a Stream<List<User>>. The UI will automatically update whenever the database table changes.
// One-time fetch
Future<List<User>> getAllUsers() => select(users).get();

// Reactive fetch (Highly Recommended for Flutter)
Stream<List<User>> watchAllUsers() => select(users).watch();

Update: Modifying Records

For updates, you can replace a whole row or update specific columns using a where clause.

Future updateUser(User entry) {
  // replace() uses the primary key to find and update the row
  return update(users).replace(entry);
}

// Targeted Update
Future updateName(int id, String newName) {
  return (update(users)..where((t) => t.id.equals(id)))
      .write(UsersCompanion(userName: Value(newName)));
}

Delete: Removing Records

Deleting data is straightforward. You can delete a specific object or use a query to delete multiple rows based on a condition.

Future deleteUser(User entry) {
  return delete(users).delete(entry);
}

// Delete with a condition
Future deleteUnderageUsers() {
  return (delete(users)..where((t) => t.userAge.isSmallerThanValue(18))).go();
}

Troubleshooting Common Flutter Drift Errors

Even with a perfect setup, Drift’s code generation can sometimes run into issues. Here are the most common “gotchas” and how to fix them quickly.

1. The .g.dart file is not generating

This is the #1 issue developers face. It usually happens because of a conflict in the build cache.

  • The Fix: Run the “clean build” command in your terminal:
flutter pub run build_runner build --delete-conflicting-outputs

Check: Ensure you have part 'database.g.dart'; at the very top of your file. If your file is named user_db.dart, the part must be user_db.g.dart.

2. Invalid argument: Instance of UsersCompanion

If you try to use a User object for an insertion or a UsersCompanion for a return type, Drift will throw an error.

  • The Fix: * Use UsersCompanion for Inserts (into the DB).
    • Use User for Data Fetching (out of the DB).

3. No such table: users (Migration Error)

If you added a new column or changed a field name and the app crashes, it’s because the SQLite schema on your device doesn’t match your code.

  • The Fix: You must increment your schemaVersion.
@override
int get schemaVersion => 2; // Increase this by 1

Pro Tip: For production apps, implement a MigrationStrategy to ensure users don’t lose their data when you update the app.

4. Database not found on Web/Mobile

Drift uses different “drivers” for different platforms.

  • The Fix: Ensure you are using NativeDatabase for Android/iOS and WasmDatabase or WebDatabase for Flutter Web. Using the wrong one will result in a “Missing implementation” error.

pubspec.yaml

Add drift database plugin to your project check the official page for latest version updates.

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  drift: ^2.28.1
  path_provider: ^2.1.5
  path: ^1.9.1

users.dart

Create a users table where we have fields id, name and age.

import 'package:drift/drift.dart';

class Users extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text().withLength(min: 3, max: 50)();
  IntColumn get age => integer().nullable()();
}

app_database.dart

Creating a database file where we customize the drift database with the required CRUD operations.

import 'dart:io';

import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'users.dart';
import 'package:path/path.dart' as p;
part 'app_database.g.dart';


@DriftDatabase(tables: [Users])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;

  Future<int> insertUser(UsersCompanion user) {
    return into(users).insert(user);
  }

  Future<List<User>> getAllUsers(){
    return select(users).get();
  }

  Future<bool> updateUser(User user) => update(users).replace(user);

  Future<int> deleteUser(int id){
    return (delete(users)..where((tbl)=> tbl.id.equals(id))).go();
  }

  Future<List<User>> getUsersAboveAge(){
    return (select(users)..where((u) => u.age.isBiggerThanValue(27))).get();
  }

  Future<List<User>> getUserOrderByAge(){
    return (select(users)..orderBy([(u) => OrderingTerm(expression: u.age, mode: OrderingMode.desc)])).get();
  }
}


LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'app.db'));
    return NativeDatabase(file);
  });
}

Now we have to generate app_database.g.dart using the below command running through terminal. may refer video tutorial there i have clearly explained generating the file.

flutter pub run build_runner build

userpage.dart

In this page we have a designed a simple user interface to insert, delete & update the user information.

import 'package:drift/drift.dart' as d;
import 'package:flutter_drift/main.dart';
import 'package:flutter/material.dart';

import 'app_database.dart';

class UserPage extends StatefulWidget {
  final User user;
  const UserPage({super.key, required this.user});

  @override
  State<UserPage> createState() => _UserPageState();
}

class _UserPageState extends State<UserPage> {
  final db = database;
  late TextEditingController _nameController;
  late TextEditingController _ageController;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _nameController = TextEditingController(text: widget.user.name);
    _ageController = TextEditingController(text: widget.user.age.toString());
  }

  Future<void> _updateuser() async {
    final updatedUser = widget.user.copyWith(
      name: _nameController.text,
      age: d.Value(int.tryParse(_ageController.text)),
    );
    await db.updateUser(updatedUser);

    await Navigator.push(context, MaterialPageRoute(builder: (_) => MyApp()));
  }

  Future<void> _deleteUser() async {
    await db.deleteUser(widget.user.id);
    await Navigator.push(context, MaterialPageRoute(builder: (_) => MyApp()));
  }

  Future<void> _addUser() async {
    await db.insertUser(UsersCompanion.insert(name: _nameController.text, age: d.Value(int.tryParse(_ageController.text))));
    await Navigator.push(context, MaterialPageRoute(builder: (_) => MyApp()));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("User details"),),
      body: Padding(
          padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          TextField(
            controller: _nameController,
            decoration: const InputDecoration(labelText: "Name"),
          ),
          const SizedBox(height: 10),
          TextField(
            controller: _ageController,
            decoration: const InputDecoration(labelText: "Age"),
          ),
          const SizedBox(height: 20),
          Row(
            children: [
              ElevatedButton(onPressed: _updateuser, child: const Text("Update")),
              const SizedBox(height: 20),
              ElevatedButton(onPressed: _deleteUser, child: const Text("Delete")),
              const SizedBox(height: 20),
              ElevatedButton(onPressed: _addUser, child: const Text("Add"))
            ],
          )
        ],
      ),
      ),
    );
  }
}

Creating Database Views

import 'package:drift/drift.dart';

part 'app_database.g.dart';

class Products extends Table {

  IntColumn get id =>  integer().autoIncrement()();
  TextColumn get title => text()();
  TextColumn get description => text()();
}

abstract class ProductsView extends View{
  Products get products;

  @override
  Query as() =&gt; select([
    products.title
  ]).from(products);
}

@DriftDatabase(tables:[
  Products
], views:[
  ProductsView
])

class Database extends _$Database {
  Database(QueryExecutor e): super(e);

  @override
  int get schemaVersion =&gt; 2;
}

main.dart

In the main.dart file we try loading the list view so that we can display the user information.

import 'package:drift/drift.dart' as d;
import 'package:flutter/material.dart';
import 'package:flutter_drift/database/app_database.dart';

import 'database/userpage.dart';

late AppDatabase database;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  database = AppDatabase();

  runApp(MyApp());
}

class MyApp extends StatefulWidget {

  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late Future<List<User>> _usersList;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    setState(() {
      _usersList = database.getAllUsers();
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Drift CRUD Example',
      home: Scaffold(
        appBar: AppBar(title: Text("Home")),
        body: Column(
          children: [
            TextButton(onPressed: (){
              setState(() {
                _usersList = database.getUsersAboveAge();
              });
            }, child: Text("Filter")),
            TextButton(onPressed: (){
              setState(() {
                _usersList = database.getUserOrderByAge();
              });
            }, child: Text("Order By")),
           Expanded(child:  displayListView(),)
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () async {
            database.insertUser(UsersCompanion.insert(
                name: "Abhi",
            age: d.Value(25)));
            setState(() {});
          },
          child: Text("+"),
        ),
      ),
    );
  }

  FutureBuilder<List<User>> displayListView() {
    return FutureBuilder(
        future: _usersList,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Center(child: CircularProgressIndicator());
          }
          final users = snapshot.data;

          return ListView.builder(
            itemCount: users?.length,
            itemBuilder: (context, index) {
              return ListTile(
                  title: Text(users![index].name),
                subtitle: Text(users[index].age.toString()),
                onTap: () async {
                    await Navigator.push(context,
                    MaterialPageRoute(builder: (_) => UserPage(user: users[index],)));
                },
              );
            },
          );
        },
      );
  }
}

Complete Playlist :

Here’s the complete curated playlist have a look through…

FAQ

Q1: What is the difference between Drift and SQFlite?

Drift is a wrapper around SQFlite that adds type-safety and reactive streams. While SQFlite requires manual SQL strings, Drift allows you to write queries in pure Dart, reducing runtime errors.

Q2: Does Drift support Flutter Web?

Yes, Drift supports Flutter Web using WebAssembly (Wasm). You will need to add the drift_dev and sqlite3 Wasm files to your web folder to enable persistence in the browser.

Q3: How do I handle database migrations in Drift?

Drift has a built-in MigrationStrategy within the SchemaVersion getter. You can use the onUpgrade callback to add new columns or tables without losing existing user data.

Leave a Comment