Drift Database Deep Dive: Optimizing Performance for Flutter on Web and Mobile🗄️🚀

Neqz
7 min readFeb 20, 2024

In the ever-evolving landscape of app development, databases play a fundamental role in managing and storing application data efficiently. For Flutter developers seeking robust solutions to handle data management across web and mobile platforms, Drift emerges as a promising contender. Drift, purpose-built for Flutter apps, offers a seamless and intuitive approach to database integration, empowering developers to streamline data operations with ease.

In this comprehensive guide, we delve into the world of Drift database and its versatile support for Flutter on both web and mobile platforms. Whether you’re a seasoned developer looking to enhance your app’s performance or a newcomer eager to explore the realm of database management, this article serves as your roadmap to harnessing the full potential of Drift. One of the most significant advantages of Drift is its indexed database structure and ease of use compared to other libraries.

Drift is purpose-built for Flutter apps, which means it is optimized for performance and seamless integration with Flutter’s ecosystem. This specialization can lead to better compatibility and efficiency compared to more generic database solutions. And it benefits from an active community of developers and contributors, providing ongoing support, updates, and enhancements. This vibrant community can be invaluable for troubleshooting issues, sharing knowledge, and collaborating on improvements. Plus, Drift’s indexed database structure is a game-changer. It speeds up query execution and enhances search capabilities, resulting in quicker data retrieval and improved overall performance.

Basic implementation:

We’ll review the implementation based on an example from the Drift package, enhancing it for even greater convenience, simplicity, and clarity in working with the database.

To begin, we’ll incorporate the required libraries.

dependencies:
flutter:
sdk: flutter
...
drift: ^2.14.0
sqlite3_flutter_libs: ^0.5.20

dev_dependencies:
build_runner: ^2.4.8
build_web_compilers: ^4.0.3
drift_dev: ^2.14.0
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0

It’s imperative to highlight the incorporation of the sqlite3_flutter_libs, particularly when handling databases in mobile applications. For web application development, it’s essential to include build_web_compilers in dev_dependencies.

Moving forward, the next step involves contemplating the implementation of the main file for Drift — database.dart.

part 'database.g.dart';

@DriftDatabase(
tables: [TodoEntries, Categories],
daos: [
AllTodoEntries,
AllCategories
],
)
class MyDatabase extends _$MyDatabase {
MyDatabase(QueryExecutor executor) : super(executor);

@override
int get schemaVersion => 1;

@override
MigrationStrategy get migration {
return MigrationStrategy(beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
}, onCreate: (m) async {
await m.createAll();
}
);
}
}

The DriftDatabase annotation enumerates Data Access Objects and tables. This structured approach enables the package to generate and create a database in the application cache upon initial launch.

Moving forward, the next essential component is defining a table:

@DataClassName('TodoEntry')
class TodoEntries extends Table with AutoIncrementingPrimaryKey {
TextColumn get description => text()();
IntColumn get category => integer().nullable().references(Categories, #id)();
DateTimeColumn get dueDate => dateTime().nullable()();
}

In the file, it is crucial to specify the name of the table for the database within the annotation (DataClassName) and define the fields within the class.

Additionally, going beyond the initial query, AutoIncrementingPrimaryKey is a custom mixin from the example that appends a primary key to the table.

mixin AutoIncrementingPrimaryKey on Table {
IntColumn get id => integer().autoIncrement()();
}

And lastly, the Data Access Object (DAO):

The adoption of DAOs is rooted in their ability to centralize data access operations, thereby promoting a structured and manageable approach to database interaction. By encapsulating database queries and operations within dedicated DAO classes, we create a cleaner and more modular codebase, mitigating the risk of code duplication and enhancing maintainability.

Furthermore, DAOs align with best practices such as the separation of concerns and the single responsibility principle. Through the segregation of database interactions from core application logic, we achieve clarity and comprehensibility within the codebase. This separation empowers developers to focus on implementing and maintaining business logic without being encumbered by database intricacies.

Moreover, DAOs facilitate robust testing practices by simplifying unit testing and mocking of database interactions. With database operations encapsulated within DAO methods, we can conduct targeted unit tests to verify each method’s functionality independently of the underlying database. This enhances our ability to identify and address issues early in the development process, thereby improving code quality and reliability.

In addition to enhancing maintainability and testability, DAOs offer flexibility and scalability in adapting to evolving business requirements. By abstracting database interactions behind DAO interfaces, we can accommodate changes to our data model or database schema with minimal disruption to the overall codebase. This flexibility ensures that our application remains agile and responsive to changing needs over time.

In conclusion, the adoption of Data Access Objects represents a strategic decision to optimize database management within our Flutter application. By leveraging DAOs, we create a more structured, maintainable, and testable codebase while also ensuring flexibility and scalability to accommodate future growth and evolution. Implementing DAOs aligns with our commitment to excellence in software development and lays a solid foundation for the continued success of our application.

part 'all_todo_entries.g.dart';

@DriftAccessor(tables:[TodoEntries, Categories])
class AllTodoEntries extends DatabaseAccessor<MyDatabase> with _$AllTodoEntriesMixin {
AllTodoEntries(MyDatabase attachedDatabase) : super(attachedDatabase);

Stream<List<TodoEntryWithCategory>> entriesInCategory(int? categoryId) {
final query = select(todoEntries).join([
leftOuterJoin(categories, categories.id.equalsExp(todoEntries.category))
]);

if (categoryId != null) {
query.where(categories.id.equals(categoryId));
} else {
query.where(categories.id.isNull());
}

return query.map((row) {
return TodoEntryWithCategory(
entry: row.readTable(todoEntries),
category: row.readTableOrNull(categories),
);
}).watch();
}

Future<int> updateTodoEntry(TodoEntriesCompanion companion) {
return (update(todoEntries)
..where((tbl) => tbl.id.equals(companion.id.value)))
.write(companion);
}

Future<int> createTodoEntry(TodoEntriesCompanion companion) {
return into(todoEntries).insert(companion);
}

Future<int> deleteTodoEntry(int entryId) async {
return transaction(() async {
return await (delete(todoEntries)
..where((tbl) => tbl.id.equals(entryId)))
.go();
});
}
}

The DAO serves as a pivotal component in database interaction, where essential queries are implemented. Within the DriftAccessor annotation, it’s imperative to specify the tables utilized in your object. Failure to do so will render any unspecified table inaccessible from the DAO.

Attention! DAO and DriftDatabase require file generation after any changes.

Enhancements are needed to support the web platform. Primarily, custom configuration for the build runner must be incorporated:

targets:
$default:
builders:
drift_dev:
generate_for:
- lib/**.dart
options:
named_parameters: true
build_web_compilers:entrypoint:
generate_for:
- web/**.dart
options:
compiler: dart2js
dev_options:
dart2js_args:
- --no-minify
release_options:
dart2js_args:
- -O4

Let’s leverage the Bash script provided in the Drift example for enhanced convenience:

rm -f web/worker.dart.js
rm -f web/worker.dart.min.js
dart run build_runner build --delete-conflicting-outputs -o web:build/web/
cp -f build/web/worker.dart.js web/worker.dart.js
rm -rf build/web
dart run build_runner build --release --delete-conflicting-outputs -o web:build/web/
cp -f build/web/worker.dart.js web/worker.dart.min.js
rm -rf build/web

We’ve streamlined the process, enabling direct file generation through the Bash script: ./generate.sh run.

Launch and use:

In order to proceed, our first step is to establish a connection with the database, tailored to the specific platform. We will address the implementation requirements for both mobile and web applications.

General:

import 'package:drift/drift.dart';

import 'platform_stub.dart'
if (dart.library.io) 'platform_app.dart'
if (dart.library.html) 'platform_web.dart';

class DBCreator {
static QueryExecutor createDatabaseConnection(String databaseName) =>
PlatformInterface.createDatabaseConnection(databaseName);
}

Mobile:

import 'dart:io';

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

class PlatformInterface {
static QueryExecutor createDatabaseConnection(String databaseName) {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(join(dbFolder.path, '$databaseName.sqlite'));
return NativeDatabase(file);
});
}
}

Web:

import 'package:drift/drift.dart';
import 'package:drift/web/worker.dart';
import 'package:flutter/foundation.dart';

class PlatformInterface {
static QueryExecutor createDatabaseConnection(String databaseName) {
return DatabaseConnection.delayed(connectToDriftWorker(
kReleaseMode ? 'worker.dart.min.js' : 'worker.dart.js',
mode: DriftWorkerMode.shared));
}
}

Unsupported:

import 'package:drift/drift.dart';

class PlatformInterface {
static QueryExecutor createDatabaseConnection(String databaseName) =>
throw UnsupportedError(
'Cannot create a client without dart:html or dart:io');
}

Utilizing a singleton pattern for database connection is essential. With dependency injection using GetIt, we ensure a centralized and efficient approach to accessing the database throughout our application. Let’s proceed with implementing this approach for optimal performance and maintainability:

final di = GetIt.instance;

class DependencyInjectionManager {
static Future<void> initDependencies() async {
registerDatabase();
}

static void registerDatabase() {
di.registerLazySingleton(() => MyDatabase(DBCreator.createDatabaseConnection('dbname')));
}
}

After setting up the singleton for the database connection, the next step is to initialize our dependencies in the main function:

void main() async {
await DependencyInjectionManager.initDependencies();
runApp(MaterialApp.router(
title: 'Flutter drift demo',
routerConfig: AppRouter.router,
));
}

Now we can call the database connection instance using di: di<MyDatabase>().allTodoEntries.createTodoEntry(...).

Importing and Exporting Databases:

To facilitate file handling, the initial step is to include several packages:

  1. file_picker
  2. file_picker_writable
  3. path_provider
  4. archive

Afterward, we can proceed to add functions for importing and exporting database files:

import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:file_picker/file_picker.dart';
import 'package:file_picker_writable/file_picker_writable.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

class DbAndFilesTransfer {

Future<void> exportToDir() async {
Directory dir = await getTemporaryDirectory();
String filename = 'dbname-data-dumb-${_getTimestamp()}.zip';
String pathToZip = p.join(dir.path, filename);
File dbFile = await _getDbFile();
String appPath = p.join((await getApplicationDocumentsDirectory()).path, 'projects');
Directory appDir = Directory(appPath);

var encoder = ZipFileEncoder();
encoder.create(pathToZip);

if (appDir.existsSync()) {
encoder.addDirectory(appDir);
}

encoder.addFile(dbFile);
encoder.close();

File zip = File(pathToZip);

final fileInfo = await FilePickerWritable().openFileForCreate(
fileName: filename,
writer: (file) async {
zip.copySync(file.path);
},
);
if (fileInfo == null) {
return;
}

zip.deleteSync();

return;
}

Future<void> importToDir() async {
final result = await FilePicker.platform.pickFiles(allowMultiple: false);
if(result != null){
var decoder = ZipDecoder();
final inputStream = InputFileStream(result.files[0].path!);
final archive = decoder.decodeBuffer(inputStream);
for (var file in archive.files) {
if (file.isFile) {
if(file.name.contains('dbname.sqlite')){
_importDBFile(file);
}
}
}
}
}

String _getTimestamp() {
DateTime dateTime = DateTime.now();
return '${dateTime.year}-${dateTime.month}-${dateTime.day}_${dateTime.hour}-${dateTime.minute}';
}

Future<bool> _importDBFile(ArchiveFile importedFile) async{
File file = await _getDbFile();
try {
return file.writeAsBytes(await importedFile.content, flush: true).then((value) => true)
.onError((error, stackTrace) {
return Future.error("Error $error");
});
} on Exception catch (error) {
return Future.error("Error $error");
}
}

static Future<File> _getDbFile() async {
var dir = await getApplicationDocumentsDirectory();
final String path = '${dir.path}/dbname.sqlite';
final File file = File(path);
return file;
}
}

In conclusion, our exploration of the Drift database for Flutter applications has shed light on its significant advantages and versatile capabilities. As we’ve navigated through the intricacies of database integration, from basic implementation to advanced features like Data Access Objects (DAOs), it’s evident that Drift offers a robust solution for managing data across web and mobile platforms.

Drift’s indexed database structure, ease of use, and comprehensive support for Flutter make it a compelling choice for developers seeking efficient and reliable database management. By leveraging Drift, developers can streamline data operations, enhance performance, and maintain code quality with ease.

In essence, our deep dive into the Drift database underscores its significance as a cornerstone of efficient data management in Flutter development. With Drift, developers have the tools they need to elevate their applications to new heights of performance and functionality, driving success in the ever-evolving landscape of app development.

You can see an full example by clicking here.

--

--