Flutter Drift (Moor)

Reactive persistence library for Flutter and Dart

🌊 What is Drift?

Drift (formerly Moor) is a reactive persistence library built on SQLite. It provides type-safe database access, automatic code generation, reactive queries with streams, and powerful migration tools for building robust Flutter applications.


// Add to pubspec.yaml
dependencies:
  drift: ^2.13.0
  sqlite3_flutter_libs: ^0.5.0

// Define tables with Dart classes
class Users extends Table { ... }
                                    

Key Features

🔒

Type Safe

Compile-time checked queries

select(users).get();
🔄

Reactive

Automatic UI updates with streams

select(users).watch();
⚙️

Code Generation

Auto-generates database code

part 'database.g.dart';
🔀

Migrations

Easy database version management

onUpgrade: (m, from, to) {
  // Migrate
}

🔹 Installation

Add Drift packages to your project:

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  drift: ^2.13.0
  sqlite3_flutter_libs: ^0.5.0
  path_provider: ^2.1.1
  path: ^1.8.3

dev_dependencies:
  drift_dev: ^2.13.0
  build_runner: ^2.4.6

drift_dev generates database code automatically

🔹 Define Tables

Create table definitions using Dart classes:

import 'package:drift/drift.dart';

// Define the users table
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)();
}

// Define the tasks table
class Tasks extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text()();
  BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
  IntColumn get userId => integer().references(Users, #id)();
}

🔹 Create Database

Set up your database class:

import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

part 'database.g.dart'; // Generated file

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

  @override
  int get schemaVersion => 1;

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

Run flutter pub run build_runner build to generate code

🔹 Insert Data

Add records to your database:

final database = AppDatabase();

// Insert a user
Future<int> addUser(String name, String email, int age) async {
  return await database.into(database.users).insert(
    UsersCompanion(
      name: Value(name),
      email: Value(email),
      age: Value(age),
    ),
  );
}

// Usage
int userId = await addUser('Alice', '[email protected]', 28);
print('User added with ID: $userId');

Output:

User added with ID: 1

🔹 Query Data

Retrieve data with type-safe queries:

// Get all users (one-time)
Future<List<User>> getAllUsers() async {
  return await database.select(database.users).get();
}

// Watch users (reactive - updates automatically)
Stream<List<User>> watchAllUsers() {
  return database.select(database.users).watch();
}

// Get user by ID
Future<User?> getUserById(int id) async {
  return await (database.select(database.users)
    ..where((u) => u.id.equals(id)))
    .getSingleOrNull();
}

// Get users with condition
Future<List<User>> getAdultUsers() async {
  return await (database.select(database.users)
    ..where((u) => u.age.isBiggerOrEqualValue(18))
    ..orderBy([(u) => OrderingTerm(expression: u.name)]))
    .get();
}

🔹 Update and Delete

Modify and remove records:

// Update user
Future<bool> updateUser(int id, String newName) async {
  return await (database.update(database.users)
    ..where((u) => u.id.equals(id)))
    .write(UsersCompanion(name: Value(newName)));
}

// Delete user
Future<int> deleteUser(int id) async {
  return await (database.delete(database.users)
    ..where((u) => u.id.equals(id)))
    .go();
}

// Delete all users
Future<int> deleteAllUsers() async {
  return await database.delete(database.users).go();
}

🔹 Reactive UI Example

Build UI that updates automatically:

class UserListPage extends StatelessWidget {
  final AppDatabase database = AppDatabase();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Users')),
      body: StreamBuilder<List<User>>(
        stream: database.watchAllUsers(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Center(child: CircularProgressIndicator());
          }

          final users = snapshot.data!;

          if (users.isEmpty) {
            return 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('Age: ${user.age ?? "N/A"}'),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Add user - UI updates automatically!
          database.addUser('New User', '[email protected]', 25);
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

🔹 Joins and Complex Queries

Perform advanced queries with joins:

// Get users with their tasks
Future<List<UserWithTasks>> getUsersWithTasks() async {
  final query = database.select(database.users).join([
    leftOuterJoin(
      database.tasks,
      database.tasks.userId.equalsExp(database.users.id),
    ),
  ]);

  final results = await query.get();
  
  // Process results...
  return results.map((row) {
    return UserWithTasks(
      user: row.readTable(database.users),
      tasks: row.readTableOrNull(database.tasks),
    );
  }).toList();
}

🔹 Best Practices

  • Use Streams: Leverage reactive queries for automatic UI updates
  • Companions: Use Companion classes for inserts/updates
  • Transactions: Wrap multiple operations in transactions
  • Migrations: Plan schema changes carefully
  • Close Database: Dispose database when app closes

🧠 Test Your Knowledge

What is the main advantage of Drift over raw SQLite?