In Flutter app development, managing local data efficiently is crucial for creating responsive and feature-rich applications. Drift, formerly known as Moor, is a powerful and reactive persistence library for Flutter and Dart. It allows you to define your database schema in Dart and automatically generates boilerplate code, making database operations type-safe and straightforward. This blog post provides a comprehensive guide on how to use Drift for local database management in Flutter.
What is Drift?
Drift is a persistence library for Dart and Flutter that simplifies working with SQLite databases. It offers a type-safe, reactive, and efficient way to interact with local databases. Key features of Drift include:
- Type-Safe: Define database schemas in Dart and enjoy type-safe queries and data models.
- Reactive: Provides streams of data changes, making it easy to build reactive UIs.
- Code Generation: Automatically generates boilerplate code for database operations.
- SQL DSL: Use a powerful SQL DSL (Domain Specific Language) to write complex queries.
- Offline Support: Enables efficient local data caching and manipulation.
Why Use Drift?
- Simplified Database Management: Automates the process of database schema definition and query generation.
- Improved Performance: Provides efficient data access and caching mechanisms.
- Reactive Programming: Integrates seamlessly with streams and reactive Flutter widgets.
- Reduced Boilerplate Code: Minimizes the amount of manual SQL code you need to write.
How to Implement Drift in Flutter
To implement Drift in your Flutter project, follow these steps:
Step 1: Add Dependencies
Add the necessary dependencies to your pubspec.yaml
file:
dependencies:
drift: ^2.0.0
sqlite3_flutter_libs: ^0.5.0 # Optional: Bundles SQLite for mobile platforms
path_provider: ^2.0.0
dev_dependencies:
drift_dev: ^2.0.0
build_runner: ^2.0.0
Explanation of dependencies:
drift
: The core Drift library.sqlite3_flutter_libs
: Bundles SQLite libraries for Flutter apps, making deployment easier (optional but recommended).path_provider
: Used to get platform-specific locations for storing the database.drift_dev
: The development tool for generating Drift code.build_runner
: A generic tool to run code generators like Drift.
Run flutter pub get
to install the dependencies.
Step 2: Define Your Database Schema
Create a Dart file (e.g., database.dart
) to define your database schema using Drift’s DSL.
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';
class Tasks extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 50)();
BoolColumn get completed => boolean().withDefault(const Constant(false))();
DateTimeColumn get dueDate => dateTime().nullable()();
}
@DriftDatabase(tables: [Tasks])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) async {
await m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
// Add migration steps here if needed
},
);
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase.createInBackground(file);
});
}
Explanation:
Tasks
: A table definition extendingTable
from Drift.id
,name
,completed
,dueDate
: Columns within theTasks
table with specified types and constraints.AppDatabase
: The main database class annotated with@DriftDatabase
, specifying the tables it contains.schemaVersion
: The version of the database schema._openConnection
: A helper function to open the SQLite database usingNativeDatabase
, which provides a platform-specific implementation.drift_dev
requires to name your class prefixed with_$
symbol (Ex: _$AppDatabase), Drift plugin renames with right ClassName on generate code
Step 3: Generate the Drift Code
Run the code generation tool to generate the necessary boilerplate code. Open your terminal, navigate to your Flutter project’s root directory, and run:
flutter pub run build_runner build
or for continuous code generation during development:
flutter pub run build_runner watch
This command processes your database.dart
file and generates the corresponding database.g.dart
file containing the necessary database classes and methods.
Step 4: Use the Generated Database Class
Now you can use the generated database class to perform database operations. Here’s how to perform CRUD (Create, Read, Update, Delete) operations:
Initialize the Database
import 'package:flutter/material.dart';
import 'package:drift_example/database.dart'; // Replace with your actual path
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final AppDatabase database = AppDatabase();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Drift Example'),
),
body: Center(
child: Text('Drift Database Example'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Example CRUD operations will be added here
},
child: Icon(Icons.add),
),
),
);
}
@override
void dispose() {
database.close();
super.dispose();
}
}
Create (Insert) Data
ElevatedButton(
onPressed: () async {
final task = TasksCompanion.insert(
name: Value('Buy groceries'),
dueDate: Value(DateTime.now().add(Duration(days: 2))),
);
await database.into(database.tasks).insert(task);
print('Task added');
},
child: Text('Add Task'),
)
Read (Select) Data
FutureBuilder>(
future: database.select(database.tasks).get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
final tasks = snapshot.data!;
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.name),
subtitle: Text('Due Date: ${task.dueDate}'),
);
},
);
} else {
return Text('No tasks found');
}
},
)
Update Data
ElevatedButton(
onPressed: () async {
final taskToUpdate = Task(
id: 1, // Replace with the actual task ID
name: 'Updated task name',
completed: true,
dueDate: DateTime.now(),
);
await database.update(database.tasks).replace(taskToUpdate);
print('Task updated');
},
child: Text('Update Task'),
)
Delete Data
ElevatedButton(
onPressed: () async {
await (database.delete(database.tasks)
..where((t) => t.id.equals(1))) // Replace 1 with the task ID
.go();
print('Task deleted');
},
child: Text('Delete Task'),
)
Step 5: Using Streams for Reactive Updates
Drift provides streams that emit data whenever changes occur in the database. This makes it easy to update your UI reactively.
StreamBuilder>(
stream: database.select(database.tasks).watch(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final tasks = snapshot.data!;
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.name),
subtitle: Text('Due Date: ${task.dueDate}'),
);
},
);
} else {
return CircularProgressIndicator();
}
},
)
In this example, database.select(database.tasks).watch()
returns a Stream<List<Task>>
that emits the latest list of tasks whenever the database changes.
Advanced Drift Features
Custom Queries
Drift allows you to write custom SQL queries for more complex data retrieval or manipulation. Use the customSelect
method to execute SQL queries directly.
final result = await database.customSelect(
'SELECT * FROM tasks WHERE completed = ?',
variables: [Variable.withBool(true)],
).get();
for (final row in result) {
print('Task Name: ${row.readString('name')}');
}
Transactions
Drift supports transactions to ensure data consistency. Use the transaction
method to execute a series of database operations as a single atomic operation.
await database.transaction(() async {
// Perform multiple database operations here
await database.into(database.tasks).insert(
TasksCompanion.insert(
name: Value('Task 1'),
),
);
await database.into(database.tasks).insert(
TasksCompanion.insert(
name: Value('Task 2'),
),
);
});
Conclusion
Drift (formerly Moor) is an excellent choice for local database management in Flutter, offering type safety, reactivity, and efficient code generation. By following the steps outlined in this guide, you can seamlessly integrate Drift into your Flutter projects, simplify your database operations, and improve your app’s performance and maintainability.