Drift Database for Flutter – A Powerful Tool for your Next App

Drift, previously called Moor, a reactive persistence library, is an excellent wrapper around SQFite and other related database tools, designed to enhance the capabilities and features that may be missed out in the above-mentioned tools. Additionally, Drift Database provides various other interesting features that make it unique in terms of its performance. So let’s dive in-depth thoroughly and get to know Drift Database for Flutter – A Powerful Tool for your Next App.


Interesting Features of Drift

Drift comes up with additional features such as:

Type Safety:

Drift lets you write queries in SQL and even generates type-safe wrappers. Additionally, we can write tables and queries in Dart without losing type-safe patterns.

Stream Responses:

Drift supports stream query responses when there comes any change in data. It doesn’t matter how complex data can be.

Drift is flexible:

Drift generates APIs during compile time to see whether the query has been written in either SQL or Dart itself.

Fast & Powerful:

Another thing that separates Drift from the rest is its support for multiple isolates.

Note: Isolate lets you perform a specific task in a separate memory space. Moreover, Flutter doesn’t support the concept of multiple isolates.

Support for multiple platforms:

Drift supports major platforms like Android, iOS, the Web, macOS, Linux, and Windows.

Another interesting aspect of Drift is that it doesn’t even require Flutter itself, but any platform acquiring Dart as their primary Programming Language.

Let’s now look at the project scenario for our Drift db.

Project Scenario

Our project shall consist of a CRUD for Memory Saver Application that would have the following:

  • ID
  • TITLE
  • WHEN (datetime)
  • PICTURE
  • ANY NOTES

Let’s now look at Drift’s current implementation.

Drift Setup & Implementation

Under the pubspec.yaml file, add the following packages to the dependencies section:

drift:
sqlite3_flutter_libs:
path_provider:
path:

And in dev_dependencies:

Let’s take a look at what sort of task these packages perform.

drift_dev:
build_runner:

—————- OR —————-

Try out this command to install the suitable version w.r.t Flutter version.

flutter pub add package_title

driftCore package for defining the most APIs

sqlite3_flutter_libs – Ships the latest sqlite3 with your Android or iOS app

Note: Doesn’t require when not using Flutter.

path_provider vis-à-vis path – Used to get a directory to store the DB file

drift_dev – A query generator tool for drift

build_runner – A common tool for code generation

Database File {.drift}:

Under the lib folder, create a new file memory_saver.drift. This file shall include the columns for our MemorySaver database. Additionally, the structure is very similar to creating tables in Sqflite.

CREATE TABLE memorysaver (
    id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
    title TEXT,
    memory_dt TEXT,
    picture TEXT,
    notes TEXT
);

Next, create a new file memory_saver.dart and add the following snippet:

Note: Be sure to check for the same filenames to avoid any sort of conflict.

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

@DriftDatabase(
  include: {'memorysaver.drift'},
)

class MyDatabase extends _$MyDatabase {
  MyDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;
}

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

This snippet will be used to initialize the database and get its db file registered in our local storage.

Note: You will get an error in this import – memory_saver.d.dart. This is because we haven’t generated the code yet.

So, for that, under the project’s terminal, we run this command:

flutter pub run build_runner build

If you’ve done it all right, you will be able to see this success message.

build_runner output

Now, under the same file, memory_saver.dart, we need to jot down our CRUD functions.

Just after the schemaVersion snippet, add the following functions:

class MyDatabase extends _$MyDatabase {
  MyDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;

  Future<List<MemorysaverData>> getAllMemory() async {
    return await select(memorysaver).get();
  }

  Future<int> saveMemory(MemorysaverCompanion companion) async {
    return await into(memorysaver).insert(companion);
  }

  Future<int> deleteMemory(int id) async {
    return (delete(memorysaver)..where((val) => memorysaver.id.equals(id))).go();
  }

  Future<int> deleteAllMemory() async {
    return await delete(memorysaver).go();
  }

  Future<int> updateMemory(MemorysaverCompanion companion) async {
    return await update(memorysaver).write(MemorysaverCompanion(
      id: companion.id,
    ));
  }
}

A question might arise. What is MemorysaverCompanion? Well, the code that we just got from the generated file automatically adds a companion to our existing class name, providing all the fields that we listed while creating the table.

Now, we’re done with the database section.

It’s time to build the UI structure and fit it in.

Under lib, add a new directory as screens, followed by two files, namely memory_listing.dart and add_new_memory.dart respectively.

add_new_memory.dart

This class shall represent 3 form fields and an image.

Code snippet
import 'dart:io';

import 'package:drift/drift.dart' as d;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:memory_saver_drift/date_extension.dart';
import 'package:memory_saver_drift/main.dart';
import 'package:memory_saver_drift/memorysaver.dart';

class AddNewMemory extends StatefulWidget {
  const AddNewMemory({Key? key}) : super(key: key);

  @override
  State<AddNewMemory> createState() => _AddNewMemoryState();
}

class _AddNewMemoryState extends State<AddNewMemory> {
  File? image;

  dynamic attachImage() async {
    PickedFile? pf = await ImagePicker().getImage(source: ImageSource.camera);

    if(pf != null) {
      setState(() {
        image = File(pf.path);
      });
    }
    else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: 
      Text("No image attached"),));
    }
  }

  final TextEditingController _titleC = TextEditingController();
  final TextEditingController _memoryDateC = TextEditingController();
  final TextEditingController _notesC = TextEditingController();
  DateTime? date;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [

          Expanded(
            child: ListView(
              padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
              shrinkWrap: true,
              children: [

                InkWell(
                  onTap: () => attachImage(),
                  child: image == null ? Container(
                    height: 150,
                    width: MediaQuery.of(context).size.width,
                    decoration: BoxDecoration(
                      color: Theme.of(context).primaryColor,
                      borderRadius: BorderRadius.circular(20)
                    ),
                    child: const Align(
                      alignment: Alignment.center,
                      child: Icon(Icons.picture_in_picture_alt_outlined, color: 
                      Colors.white, size: 50,),
                    ),
                  ) : SizedBox(
                    height: 150,
                    width: MediaQuery.of(context).size.width,
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(20),
                      child: Image.file(File(image!.path), fit: BoxFit.cover,),
                    ),
                  ),
                ),
                const SizedBox(
                  height: 10,
                ),

                getField(_titleC, 'Name your memory...'),
                getField(_memoryDateC, 'Happened on...', 
                onTap: () => datePicker(), readOnly: true),
                getField(_notesC, 'Something you would like to add extra...', 
                onTap: () {}, lines: 5),
              ],
            ),
          ),

          Align(
            alignment: Alignment.bottomCenter,
            child: Container(
              margin: const EdgeInsets.all(10),
              height: 50,
              child: ElevatedButton(
                child: const Text("Add new memory"),
                onPressed: () {

                   /* Databse snippet...  */
                },
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget getField(TextEditingController controller, String hint, 
    {VoidCallback? onTap, bool readOnly = false, int? lines}) {

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: TextFormField(
        controller: controller,
        maxLines: lines,
        readOnly: readOnly,
        textInputAction: TextInputAction.next,
        decoration: InputDecoration(
          hintText: hint,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(14)
          )
        ),
        onTap: onTap,
      ),
    );
  }

  Future<dynamic> datePicker() async {
    await showCupertinoModalPopup<void>(
      context: context,
      builder: (_) {
        final size = MediaQuery.of(context).size;
        return Container(
          decoration: const BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.only(
              topLeft: Radius.circular(12),
              topRight: Radius.circular(12),
            ),
          ),
          height: size.height * 0.27,
          child: CupertinoDatePicker(
            mode: CupertinoDatePickerMode.date,
            onDateTimeChanged: (value) {
              DateTime dt = value;
              _memoryDateC.text = dt.formatter;
              setState(() {});
            },
          ),
        );
      },
    );
  }
}
UI View
add_new_memory

Note: It’s a recommended approach to define our Drift database object a single time and then use its instance globally.

For that, under main.dart, declare the MyDatabase instance:

MyDatabase? memoryDb;

void main() {
  memoryDb = MyDatabase();
  runApp(
    MaterialApp(
      home: MyApp(),
    ),
  );
}

Now under add_memory.dart, onPressed callback, add this code.

 MemorysaverCompanion model = MemorysaverCompanion(
      title: d.Value(_titleC.text),
      memoryDt: d.Value(_memoryDateC.text),
      picture: d.Value(image!.path),
      notes: d.Value(_notesC.text),
  );

memoryDb!.saveMemory(model);

We’re done with this one. Now let’s add our Memory Listings.

memory_listing.dart
Add the following code snippet
import 'package:flutter/material.dart';
import 'package:memory_saver_drift/main.dart';
import 'package:memory_saver_drift/memorysaver.dart';
import 'package:memory_saver_drift/screens/add_new_memory.dart';
import 'package:memory_saver_drift/screens/widgets/memory_view_widget.dart';

class MemoryListing extends StatelessWidget {
  const MemoryListing({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Memories"),
      ),
      body: FutureBuilder<List<MemorysaverData>>(
        future: memoryDb!.getAllMemory(),
        builder: (context, memorySnapshot) {

          if(memorySnapshot.hasData) {

            return memorySnapshot.data!.isEmpty ? const Center(
              child: Text("No Memories yet!!\nAdd some...", style: TextStyle(
                fontSize: 30
              ),),
            ) : ListView.separated(
              shrinkWrap: true,
              itemCount: memorySnapshot.data!.length,
              itemBuilder: (context, i) {
                MemorysaverData item = memorySnapshot.data![i];

                return MemoryViewWidget(item: item);
              },
              separatorBuilder: (context, i) {
                return const Divider();
              },
            );
          }
          return const SizedBox.shrink();
        },
      ),
      floatingActionButton: FloatingActionButton.extended(
        icon: const Text("Add a new Memory"),
        label: const Icon(Icons.add),
        onPressed: () => Navigator.of(context).push(
        MaterialPageRoute(builder: (_) => const AddNewMemory())),
      ),
    );
  }
}
UI View
memory_listing

Let’s now see our code in action via a video preview.

Video Preview

We’re done. Thanks for being with me till here.


Conclusion

In this blog post, Drift Database for Flutter – A Powerful Tool for your Next App, we got to see some interesting features of Drift, vis-à-vis went through some code snippets, and keynotes in between. However, if you feel something is missed or have any questions regarding the topic, feel free to jot it down in the comments section. I would be more than happy to help you.

P.S. Check out my previous blog posts

Also read – Effective Routing with Fluro – A Flutter’s Interesting Prospect

Thanks for your precious time!!

Leave a Comment