Drift
Drift is an easy-to-use, reactive, type-safe persistence library for Dart & Flutter. Built on SQLite, it provides reactive capabilities that can turn any SQL query into an auto-updating stream, offering compile-time type safety and query validation. You can write queries in both SQL and Dart, supporting complex SQL features like WITH and WINDOW clauses, making it the ideal database solution for modern mobile and desktop application development.
GitHub Overview
simolus3/drift
Drift is an easy to use, reactive, typesafe persistence library for Dart & Flutter.
Topics
Star History
Library
Drift
Overview
Drift is an easy-to-use, reactive, type-safe persistence library for Dart & Flutter. Built on SQLite, it provides reactive capabilities that can turn any SQL query into an auto-updating stream, offering compile-time type safety and query validation. You can write queries in both SQL and Dart, supporting complex SQL features like WITH and WINDOW clauses, making it the ideal database solution for modern mobile and desktop application development.
Details
Drift 2025 edition has established itself as the most robust and feature-rich SQLite-based ORM library in the Flutter & Dart ecosystem. With cross-platform support including Android, iOS, macOS, Windows, Linux, and Web, it provides a consistent database experience everywhere. Features include type-safe code generation based on table definitions and queries, comprehensive migration support, batch updates and join operations, transaction processing, and other capabilities that meet enterprise-level demands. Reactive query streams enable automatic UI updates when data changes, bringing innovation to modern application development.
Key Features
- Reactive Queries: Convert any SQL query into auto-updating streams
- Complete Type Safety: Compile-time query validation and type generation
- Cross-Platform: Full support for Android, iOS, Web, and desktop
- Rich SQL Features: Support for advanced SQL syntax like WITH and WINDOW clauses
- Comprehensive Migration: Complete support for schema changes and database migration
- Batch Processing: High-efficiency batch updates and transactions
Pros and Cons
Pros
- Easy generation of reactive streams from any query
- Significant reduction in runtime errors through compile-time type safety
- Flexible development with query writing in both SQL and Dart
- Safe schema changes through excellent migration support
- Cross-platform support with one codebase for all platforms
- Rich features for serious enterprise applications
Cons
- More complex setup compared to other libraries with higher learning curve
- Heavy development environment due to dependency on build runner and code generation
- SQLite-based limitations for advanced relational database features
- Performance constraints with large datasets
- Larger bundle size compared to other ORM libraries (like Floor)
- Increased build times due to code generation process
Reference Pages
Code Examples
Basic Setup
// pubspec.yaml
dependencies:
drift: ^2.20.3
drift_flutter: ^0.2.0
path_provider: ^2.1.4
path: ^1.9.0
dev_dependencies:
drift_dev: ^2.20.2
build_runner: ^2.4.12
// Package installation
// flutter pub get
// dart run build_runner build
Model Definition and Basic Operations
// database.dart
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'dart:io';
part 'database.g.dart';
// Table definitions
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 50)();
TextColumn get email => text().unique()();
IntColumn get age => integer().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
class Posts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 100)();
TextColumn get content => text()();
IntColumn get userId => integer().references(Users, #id)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
// Database class
@DriftDatabase(tables: [Users, Posts])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(driftDatabase(name: 'app_database'));
@override
int get schemaVersion => 1;
// Basic CRUD operations
Future<List<User>> getAllUsers() => select(users).get();
Future<User?> getUserById(int id) =>
(select(users)..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
Future<int> insertUser(UsersCompanion user) =>
into(users).insert(user);
Future<bool> updateUser(User user) =>
update(users).replace(user);
Future<int> deleteUser(int id) =>
(delete(users)..where((tbl) => tbl.id.equals(id))).go();
// Reactive queries (auto-updating streams)
Stream<List<User>> watchAllUsers() => select(users).watch();
Stream<User?> watchUserById(int id) =>
(select(users)..where((tbl) => tbl.id.equals(id))).watchSingleOrNull();
}
// Usage example
void main() async {
final database = AppDatabase();
// Insert user
await database.insertUser(UsersCompanion.insert(
name: 'John Doe',
email: '[email protected]',
age: const Value(30),
));
// Get all users
final users = await database.getAllUsers();
print('All users: $users');
// Reactive stream (auto-updates on data changes)
database.watchAllUsers().listen((users) {
print('User list updated: ${users.length} users');
});
await database.close();
}
Advanced Query Operations
// Complex query operations
extension DatabaseQueries on AppDatabase {
// Conditional queries
Future<List<User>> getUsersByAgeRange(int minAge, int maxAge) {
return (select(users)
..where((tbl) => tbl.age.isBetweenValues(minAge, maxAge))
..orderBy([(tbl) => OrderingTerm.asc(tbl.name)])
).get();
}
// JOIN operations
Future<List<UserWithPosts>> getUsersWithPosts() {
final query = select(users).join([
innerJoin(posts, posts.userId.equalsExp(users.id))
]);
return query.map((row) {
final user = row.readTable(users);
final post = row.readTable(posts);
return UserWithPosts(user: user, posts: [post]);
}).get();
}
// GROUP BY aggregation
Future<List<UserPostCount>> getUserPostCounts() {
final query = selectOnly(users)
..addColumns([users.id, users.name, posts.id.count()])
..join([leftOuterJoin(posts, posts.userId.equalsExp(users.id))])
..groupBy([users.id, users.name]);
return query.map((row) => UserPostCount(
userId: row.read(users.id)!,
userName: row.read(users.name)!,
postCount: row.read(posts.id.count()) ?? 0,
)).get();
}
// Complex conditions and subqueries
Future<List<User>> getActiveUsersWithRecentPosts() {
final recentPosts = selectOnly(posts)
..addColumns([posts.userId])
..where(posts.createdAt.isBiggerThanValue(
DateTime.now().subtract(const Duration(days: 30))
));
return (select(users)
..where((tbl) => tbl.id.isIn(recentPosts))
).get();
}
// Custom SQL (advanced operations)
Future<List<Map<String, Object?>>> getTopActiveUsers() {
return customSelect(
'''
SELECT u.id, u.name, u.email, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.created_at >= ?
GROUP BY u.id, u.name, u.email
HAVING COUNT(p.id) > ?
ORDER BY post_count DESC
LIMIT ?
''',
variables: [
Variable.withDateTime(DateTime.now().subtract(const Duration(days: 90))),
Variable.withInt(5),
Variable.withInt(10),
],
).get();
}
}
// Data class definitions
class UserWithPosts {
final User user;
final List<Post> posts;
UserWithPosts({required this.user, required this.posts});
}
class UserPostCount {
final int userId;
final String userName;
final int postCount;
UserPostCount({required this.userId, required this.userName, required this.postCount});
}
Relation Operations
// Additional relation definitions
@DriftDatabase(tables: [Users, Posts, Categories, PostCategories])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(driftDatabase(name: 'app_database'));
@override
int get schemaVersion => 2;
// Migration
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (Migrator m) async {
await m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
if (from < 2) {
await m.createTable(categories);
await m.createTable(postCategories);
}
},
);
}
}
// New tables
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 50)();
TextColumn get description => text().nullable()();
}
class PostCategories extends Table {
IntColumn get postId => integer().references(Posts, #id)();
IntColumn get categoryId => integer().references(Categories, #id)();
@override
Set<Column> get primaryKey => {postId, categoryId};
}
// Relation operation methods
extension RelationQueries on AppDatabase {
// Get all posts by user
Future<List<Post>> getPostsByUser(int userId) {
return (select(posts)
..where((tbl) => tbl.userId.equals(userId))
..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)])
).get();
}
// Get categories by post
Future<List<Category>> getCategoriesByPost(int postId) {
final query = select(categories).join([
innerJoin(
postCategories,
postCategories.categoryId.equalsExp(categories.id),
)
])..where(postCategories.postId.equals(postId));
return query.map((row) => row.readTable(categories)).get();
}
// Get posts by category
Future<List<Post>> getPostsByCategory(int categoryId) {
final query = select(posts).join([
innerJoin(
postCategories,
postCategories.postId.equalsExp(posts.id),
)
])..where(postCategories.categoryId.equals(categoryId));
return query.map((row) => row.readTable(posts)).get();
}
// Complex relations (user → posts → categories)
Future<UserWithPostsAndCategories> getUserWithPostsAndCategories(int userId) async {
final user = await getUserById(userId);
if (user == null) throw Exception('User not found');
final userPosts = await getPostsByUser(userId);
final postsWithCategories = <PostWithCategories>[];
for (final post in userPosts) {
final categories = await getCategoriesByPost(post.id);
postsWithCategories.add(PostWithCategories(
post: post,
categories: categories,
));
}
return UserWithPostsAndCategories(
user: user,
postsWithCategories: postsWithCategories,
);
}
// Complex operations within transactions
Future<void> createPostWithCategories(
int userId,
String title,
String content,
List<int> categoryIds,
) {
return transaction(() async {
// Create post
final postId = await into(posts).insert(PostsCompanion.insert(
title: title,
content: content,
userId: userId,
));
// Associate categories
for (final categoryId in categoryIds) {
await into(postCategories).insert(PostCategoriesCompanion.insert(
postId: postId,
categoryId: categoryId,
));
}
});
}
}
// Data classes
class PostWithCategories {
final Post post;
final List<Category> categories;
PostWithCategories({required this.post, required this.categories});
}
class UserWithPostsAndCategories {
final User user;
final List<PostWithCategories> postsWithCategories;
UserWithPostsAndCategories({required this.user, required this.postsWithCategories});
}
Practical Examples
// Practical Flutter application usage example
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Database provider
class DatabaseProvider extends ChangeNotifier {
late final AppDatabase _database;
AppDatabase get database => _database;
DatabaseProvider() {
_database = AppDatabase();
}
@override
void dispose() {
_database.close();
super.dispose();
}
}
// Main app
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => DatabaseProvider(),
child: MaterialApp(
title: 'Drift Demo',
home: UserListScreen(),
),
);
}
}
// User list screen (reactive)
class UserListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final database = context.read<DatabaseProvider>().database;
return Scaffold(
appBar: AppBar(title: const Text('User List')),
body: StreamBuilder<List<User>>(
stream: database.watchAllUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final users = snapshot.data ?? [];
if (users.isEmpty) {
return const Center(child: Text('No users found'));
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
trailing: Text('${user.age ?? "N/A"} years'),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => UserDetailScreen(userId: user.id),
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddUserDialog(context),
child: const Icon(Icons.add),
),
);
}
void _showAddUserDialog(BuildContext context) {
final database = context.read<DatabaseProvider>().database;
final nameController = TextEditingController();
final emailController = TextEditingController();
final ageController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('New User'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Name'),
),
TextField(
controller: emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
TextField(
controller: ageController,
decoration: const InputDecoration(labelText: 'Age'),
keyboardType: TextInputType.number,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
final age = int.tryParse(ageController.text);
await database.insertUser(UsersCompanion.insert(
name: nameController.text,
email: emailController.text,
age: age != null ? Value(age) : const Value.absent(),
));
Navigator.pop(context);
},
child: const Text('Add'),
),
],
),
);
}
}
// User detail screen
class UserDetailScreen extends StatelessWidget {
final int userId;
const UserDetailScreen({required this.userId});
@override
Widget build(BuildContext context) {
final database = context.read<DatabaseProvider>().database;
return Scaffold(
appBar: AppBar(title: const Text('User Details')),
body: StreamBuilder<User?>(
stream: database.watchUserById(userId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final user = snapshot.data;
if (user == null) {
return const Center(child: Text('User not found'));
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${user.name}', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('Email: ${user.email}'),
Text('Age: ${user.age ?? "Not set"}'),
Text('Created: ${user.createdAt.toString().split('.')[0]}'),
],
),
),
),
const SizedBox(height: 16),
Text('Posts', style: Theme.of(context).textTheme.titleMedium),
Expanded(
child: StreamBuilder<List<Post>>(
stream: database.getPostsByUser(userId).asStream(),
builder: (context, snapshot) {
final posts = snapshot.data ?? [];
if (posts.isEmpty) {
return const Center(child: Text('No posts found'));
}
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return Card(
child: ListTile(
title: Text(post.title),
subtitle: Text(
post.content,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
post.createdAt.toString().split(' ')[0],
),
),
);
},
);
},
),
),
],
),
);
},
),
);
}
}