Drift Local Database For Flutter (Part 1 — Intro, Setup and Migration)

Evagoras Frangou
7 min readJan 26, 2024

Introduction

In this article, you will learn about the Drift database. What is it, and how to use it. To be easy to follow through. This article will be split into 4 parts in which you will learn the following.
1. A few words about Drift Database, the setup and migration (PART 1)
2. How you can implement One-to-One relationship using .drift file or classes with example (
PART 2)
3. How you can implement One-to-Many relationship using .drift file or classes with example (
PART 3)
4. How you can implement Many-to-Many relationship using .drift file or classes with example (
PART 4)

A few words about the Drift database

Drift database is a reactive database which you can use in flutter apps to handle offline data. It is built on top of SQLite. Drift offers a range of significant advantages, including:

  1. Flexibility: Enables the use of both SQL and Dart (As we will see later on).
  2. Safety: As can find possible mistakes during compilation time.
  3. Migration Convenience: Facilitates easy database migration with utility functions as we will see later on.

For more information about why you should consider using Drift in your next project please check the following link: https://pub.dev/packages/drift

Before proceeding into the coding part let’s have a brief of the database scheme used in this tutorial.

The example you will follow in this tutorial

For this tutorial, you can assume that you will build a very simple music player database which consists of 4 tables.

  1. User
  2. Artist
  3. Song
  4. Playlist

from which the following relationships are established.

  1. User with Playlist One to One(For this tutorial assume that restriction is applied to the user that can have ONLY one playlist).
  2. Artist with Song One to Many
  3. Song with Playlist M to M
Database ER Diagram

Setting up the project

Step 1: Adding Dependencies:

Simply go to https://pub.dev/packages/drift/install from where you can use either the command or just copy and paste the version indicated on the website.
Except from this, you need to add some extra dependencies which are required for the drift to work properly.
1. sqlite3_flutter_libs
2. path_provider
3. path
and under the dev_dependencies you need to add two more packages
1. drift_dev
2. build_runner

Now your dependencies in .yaml file should look like this:

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
drift: ^2.14.0
sqlite3_flutter_libs: ^0.5.18
path_provider: ^2.1.1
path: ^1.8.3

dev_dependencies:
flutter_test:
sdk: flutter
drift_dev: ^2.14.1
build_runner: ^2.4.7

Step 2: Creating Tables:

To create tables in Drift there are two ways.
1. Using the .drift file: For this, you need to create a new file and use the extension .drift. Then inside the file, you can create the tables using the SQL syntax as shown in the example code below.

CREATE TABLE User (
id INT NOT NULL PRIMARY KEY,
username TEXT NOT NULL,
music_style TEXT NOT NULL,
favorite_song INT NOT NULL
) As UserTable;

CREATE TABLE Song (
id INT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
duration INT NOT NULL
) AS SongTable;

CREATE TABLE Artist (
id INT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
age INT NOT NULL,
music_style INT NOT NULL
) AS ArtistTable;

CREATE TABLE Playlist (
id INT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
number_of_songs INT NOT NULL
) AS PlaylistTable;

2. Using .dart class file: For this, it is recommended to create a class for each table. For the tutorial, I will include all classes within the same code snippet.

@UseRowClass(UserEntity)
class User extends Table {
IntColumn get id => integer()();
TextColumn get username => text()();
TextColumn get musicStyle => text()();
TextColumn get favoriteSongName => text()();

///Specifying which from the field above is the primary key
@override
Set<Column> get primaryKey => {id};
}

@UseRowClass(SongEntity)
class Song extends Table {
IntColumn get id => integer()();
TextColumn get name => text()();
IntColumn get duration => integer()();

///Specifying which from the field above is the primary key
@override
Set<Column> get primaryKey => {id};
}

@UseRowClass(ArtistEntity)
class Artist extends Table {
IntColumn get id => integer()();
TextColumn? get name => text().nullable()();
IntColumn? get age => integer().nullable()();
TextColumn? get musicStyle => text().nullable()();

///Specifying which from the field above is the primary key
@override
Set<Column> get primaryKey => {id};
}

@UseRowClass(PlaylistEntity)
class Playlist extends Table {
IntColumn get id => integer()();
TextColumn get name => text()();
IntColumn get numberOfSongs => integer()();

///Specifying which from the field above is the primary key
@override
Set<Column> get primaryKey => {id};
}

You might have noticed that above the class we have @UseRowClass(EntityClass)annotation. The purpose of this is to link it with your entity. Why do you need this? Is helpful when you have a custom object. You will see examples in the next parts of the series.

Step 3: Database Class:

For this, create a dart class extending the _$AppDatabase and annotate it with the @DriftDatabase. After configuring this class you need to execute the dart run build_runner build command to auto generate the required code. Below you can find examples of how you declare the tables when you use the .drift file and when you use the classes as well as how the AppDatabase class will look.

Example when you use .drift file.

import 'dart:io';

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

part 'appDatabase.g.dart';

@DriftDatabase(
include: {'tables.drift'},
)
class AppDatabase extends _$AppDatabase {

static AppDatabase instance() => AppDatabase();

AppDatabase() : super(_openConnection());

@override
int get schemaVersion => 1;
}

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

Example when you use the classes.

import 'dart:io';

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

part 'appDatabase.g.dart';

@DriftDatabase(
tables: [
Artist,
Playlist,
PlaylistWithSong,
Song,
User
],
)
class AppDatabase extends _$AppDatabase {

static AppDatabase instance() => AppDatabase();

AppDatabase() : super(_openConnection());

@override
int get schemaVersion => 1;
}

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

Important notes

At this point, I would like to point out 3 important things of which you should be aware.
1. part ‘appDatabase.g.dart’; → This line is important as it is used to create the auto-generated file. The name you have to use before the .g.dart is the name of your dart file. For example, in this tutorial, the name of the file is app_database.dart.
2.
When using the .drift file it is important that you keep both the .drift file and database file within the same directory.
3.
When using the classes every time you create a new table you should add it inside the tables array as shown in the example above.

Migration:

Migration needs to be handled whenever you have changes to your database scheme i.e adding a new column(field), adding a completely new table etc. To do this in the Drift database you need to override the following variable and method in your database class

  @override
int get schemaVersion => 1;

@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (Migrator m) async {
await m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
if (from < 2) {}
},
beforeOpen: (details) async {
if (kDebugMode) {
await validateDatabaseSchema();
};
},
);
}

In the code snippet above you can see we have three callbacks:
1. onCreate: This will get called only the first time when the database is created.
2. onUpgrade: This callback is getting called every time the schemaVersion is upgraded. And it is here where you will handle the migration.
3. beforeOpen: This callback is helpful as you can implement whatever you need before the database connection is open. In combination with the method, validateDatabaseSchema() is very helpful as it helps during the development phase to check if you migrated correctly. In case you have an issue with the migration you will see an error message in the terminal similar to the example below.

[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Schema does not match
E/flutter (18578): Artist:
E/flutter (18578): columns:
E/flutter (18578): comparing isActive:
E/flutter (18578): The actual schema does not contain anything with this name.

Migration Steps
After implementing the override methods the steps you need to follow are:
1. Increase the schemaVersion by 1.
2. Handle the upgrade:
For example, if the previous schemaVersion was 1 and now is 2 your code in the onUpgrade callback would be as follow.

onUpgrade: (Migrator m, int from, int to) async {
if (from < 2) {
//To migrate a new column called isActive in our artist table
await m.addColumn(artist, artist.isActive);

//To migrate a newly added table. For example Music Company
await m.create(musicCompany);
}
},

Extra Tip
In case you want to check if the data are stored correctly in your database and you want to avoid using print() or create UI. I came across this library which provides you with the functionality of accessing your database tables and checking the data stored. https://pub.dev/packages/drift_db_viewer

Conclusion

This concludes Part 1 of the Flutter Local Database Series.
If you have found this article helpful feel free to share and check out the rest parts of the series.

Suggestions and questions are welcome so, don’t hesitate to leave your comment. Of course, claps are highly appreciated as they fuel the motivation to produce more insightful articles. Thank you for your time.

GitHub Projects:
1. Example with .drift
2. Example with classes

References:
1. Drift Package on https://pub.dev/packages/drift
2. Drift Documentation https://drift.simonbinder.eu/docs/

--

--

Evagoras Frangou

BSc Computer Science, Senior Software Engineer/Team Lead, Android and Flutter Developer Find out more example projects on my GitHub: https://github.com/r1n1os