Table of Contents
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.
- 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. - 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 theTableclass to specify columns likeIntColumnorTextColumn.
Action: Annotate your database class with@DriftDatabase(tables: [YourTable]). - Step 3: Generate the Code with Build Runner
Description: Run the generator command in your terminal to create the
.g.dartfile which contains the boilerplate code and query logic.
Command:flutter pub run build_runner build - Step 4: Initialize the Database Class
Description: Create a constructor for your database class that opens the connection using
NativeDatabasefor mobile orWasmDatabasefor web.
Tip: Use a singleton pattern or a Provider to access the database instance across your Flutter app. - 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
UsersCompanionfor Inserts (into the DB).- Use
Userfor Data Fetching (out of the DB).
- Use
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
NativeDatabasefor Android/iOS andWasmDatabaseorWebDatabasefor 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() => select([
products.title
]).from(products);
}
@DriftDatabase(tables:[
Products
], views:[
ProductsView
])
class Database extends _$Database {
Database(QueryExecutor e): super(e);
@override
int get schemaVersion => 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
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.
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.
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.