Eshiet Ekemini A graduate of University of Uyo and a tech enthusiast, Ekemini has been building for mobile for two years, with a particular focus on Kotlin and Flutter.

Choosing the right database for your Flutter application

8 min read 2436

Choosing the Right Database for Your Flutter Application

We would all agree that at certain points in our mobile app development lifecycle, we consider storing and utilizing data. This is where a database comes in handy.

What is a database?

A database is a piece of software that stores and uses electronic information (data) in a structured manner, or data persistence. Data is reliably stored (persisted) and available to work with unless it is intentionally erased, in contrast to caching.

A database allows developers to use a programming language or API to save, read, modify, and remove data in the database. These activities are carried out in the background of an application, away from the view of end-users.

CRUD is a synonym for the most common database interactions and it stands for Create, Read, Update, and Delete.

Types of database

For the scope of this topic, we would focus primarily on two types of databases that can be used with mobile technologies. Databases can be classified based on a number of factors, which range from the type of data they support, how they scale, how they can be defined, and the location of their storage.

There are lots of databases out there, but we will stick with these two:

  • Nonrelational databases (NoSQL)
  • Relational databases (SQL)

In this guide, we would be exploring the types of databases in Flutter and cover how to get each of them set up in less than no time.

SQL/Relational databases

Relational databases are datasets that have relationships and values linking them to each other. They are commonly found as a set of database tables, of rows and columns. They hold information about an object and each table acts as a blueprint of the represented object. Every column in the database holds data pertaining to a particular type and the fields houses the precise value of the attribute.

The table’s rows indicate a group of interrelated values for a single data item. A primary key is a distinctive identifier assigned to each row in a table, while foreign keys are used to link rows from other tables in our database.

Without altering the database tables, this data may be accessed in a variety of ways.

We made a custom demo for .
No really. Click here to check it out.

sqflite

SQL, otherwise known as the relational database, and it is one of the most recognized types of database in any tech stack. Let’s look at how Flutter does SQL.

sqflite is basically an implementation of SQLite. It provides us with lots of functionality that gives us full control of our database and help us write queries, relationships, and every other database function that our application requires.

Let’s take a look at how to set this up in our application according to the official Flutter plugin page.

In order to use this package, we need to add the dependency in our pubspec file, as shown in the installation page:

// Get a location using getDatabasesPath
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');
// Delete the database
await deleteDatabase(path);
// open the database
Database database = await openDatabase(path, version: 1,
    onCreate: (Database db, int version) async {
  // When creating the db, create the table
  await db.execute(
      'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});
// Insert some records in a transaction
await database.transaction((txn) async {
  int id1 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
  print('inserted1: $id1');
  int id2 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
      ['another name', 12345678, 3.1416]);
  print('inserted2: $id2');
});
// Update some record
int count = await database.rawUpdate(
    'UPDATE Test SET name = ?, value = ? WHERE name = ?',
    ['updated name', '9876', 'some name']);
print('updated: $count');
// Get the records
List<Map> list = await database.rawQuery('SELECT * FROM Test');
List<Map> expectedList = [
  {'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
  {'name': 'another name', 'id': 2, 'value': 12345678, 'num': 3.1416}
];
print(list);
print(expectedList);
assert(const DeepCollectionEquality().equals(list, expectedList));
// Count the records
count = Sqflite
    .firstIntValue(await database.rawQuery('SELECT COUNT(*) FROM Test'));
assert(count == 2);
// Delete a record
count = await database
    .rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);
// Close the database
await database.close();

What we have just done above is create a defined path and storage name for our database that would serve as the database location on our device. After that, we make use of the Database class instance to open our database, and this provides us with some functionalities that include the following:

onCreate: Here we want to define the logic for our database when it is created
OnOpen: Code that runs when the database is opened

We’ve also inserted data into our database and printed out the results in our console.
Subsequently, we can also see how to update, delete, and query data, and finally close our database.

Drift

Drift, which was formerly known as Moor, is a reactive persistence library for Flutter and Dart, built on SQLite.

It is more of a wrapper around the SQlite package that provides us with the same functions and tools needed to write structured relational database queries, and it also takes time to reduce the boilerplate encountered in traditional SQLite scenarios.

The major advantage of Moor database is that it can be used alongside build_runner. You can find more information about build_runner here. With build_runner and Moor, you do not have to manually type out all your queries. You simply create a class specifying the rows and columns you want as fields in your class and let code generation using build_runner generate the needed database initialization codes.

In order to use Drift, you have to add it to your pubspec file and run the command flutter pub get to fetch your dependencies, as written in the docs here:

/////
//// For more information on using drift, please see https://drift.simonbinder.eu/docs/getting-started/
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'main.g.dart';
class TodoItems extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text()();
  TextColumn get content => text().nullable()();
}
@DriftDatabase(tables: [TodoItems])
class Database extends _$Database {
  Database(QueryExecutor e) : super(e);
  @override
  int get schemaVersion => 1;
  @override
  MigrationStrategy get migration {
    return MigrationStrategy(
      onCreate: (m) async {
        await m.createAll();
        // Add a bunch of default items in a batch
        await batch((b) {
          b.insertAll(todoItems, [
            TodoItemsCompanion.insert(title: 'A first entry'),
            TodoItemsCompanion.insert(
              title: 'Todo: Checkout drift',
              content: const Value('Drift is a persistence library for Dart '
                  'and Flutter applications.'),
            ),
          ]);
        });
      },
    );
  }
  // The TodoItem class has been generated by drift, based on the TodoItems
  // table description.
  //
  // In drift, queries can be watched by using .watch() in the end.
  // For more information on queries, see https://drift.simonbinder.eu/docs/getting-started/writing_queries/
  Stream<List<TodoItem>> get allItems => select(todoItems).watch();
}
Future<void> main() async {
  // Create an in-memory instance of the database with todo items.
  final db = Database(NativeDatabase.memory());
  db.allItems.listen((event) {
    print('Todo-item in database: $event');
  });
  // Add another entry
  await db
      .into(db.todoItems)
      .insert(TodoItemsCompanion.insert(title: 'Another entry added later'));
  // Delete all todo items
  await db.delete(db.todoItems).go();
/////

Here are a few key takes on using Moor (Drift):

It produces strongly typed results, which reduce the chances of runtime errors. It also integrates code generation to handle most of the heavy lifting involved in writing our queries. Also, it is feature rich and supported on both Android, iOS, MacOS, web, desktop, and Linux.

To read extensively about Drift, you can explore their official documentation website here.

Floor

Inspired by the Room persistence package, Floor provides a nice SQLite abstraction for your Flutter apps. It provides automatic mapping between in-memory objects and database rows, as well as full control over the database via SQL. As a result, a thorough grasp of SQL and SQLite is required to fully use Floor’s capabilities.

In order to use Floor, there are basically six steps you need to take:

  1. Add the required dependencies:///
    dependencies:
    flutter:
    sdk: flutter
    floor: ^1.2.0
    devdependencies:
    floor
    generator: ^1.2.0
    build_runner: ^2.1.2
    ///
  2. Create an entity
    Our entity is simply a class that is marked with the @entity annotation and is a representation or blueprint of what we want our database table to look like:// entity/person.dart
    import ‘package:floor/floor.dart’;
    @entity
    class Person {
    @primaryKey
    final int id;
    final String name;
    Person(this.id, this.name);
    }
    ///
  3. Create a DAO (Data Access Object)

The Data Access Object simply gives us access to the underlying SQLite database. It has an abstract class that defines the method signatures that we need to work with, and they all return a Future or Stream:

// dao/person_dao.dart
import 'package:floor/floor.dart';
@dao
abstract class PersonDao {
  @Query('SELECT * FROM Person')
  Future<List<Person>> findAllPersons();
  @Query('SELECT * FROM Person WHERE id = :id')
  Stream<Person?> findPersonById(int id);
  @insert
  Future<void> insertPerson(Person person);
}
///
  1. Create the database:///
    // database.dart
    // required package imports
    import ‘dart:async’;
    import ‘package:floor/floor.dart’;
    import ‘package:sqflite/sqflite.dart’ as sqflite;
    import ‘dao/person_dao.dart’;
    import ‘entity/person.dart’;
    part ‘database.g.dart’; // the generated code will be there
    @Database(version: 1, entities: [Person])
    abstract class AppDatabase extends FloorDatabase {
    PersonDao get personDao;
    }
    ///
  2. Run the code generator
    Run the generator with Flutter packages pub run build_runner build. To automatically run it, whenever a file changes, use flutter packages pub run build_runner watch.
  3. Use the generated code
    Anytime you need to access an instance of your database, use the generated $FloorAppDatabase class, which provides us access to the database. The name is being composed by $Floor and the database class name. The string passed to databaseBuilder() will be the database file name. For initializing the database, call build() and make sure to await the result.

In order to retrieve the PersonDao instance, calling the personDao getter on the database instance would suffice. Its functions can be used as shown below:

final database = await $FloorAppDatabase.databaseBuilder('app_database.db').build();
final personDao = database.personDao;
final person = Person(1, 'Frank');
await personDao.insertPerson(person);
final result = await personDao.findPersonById(1);
///

To get more information about Floor, you can checkout the official example repo on GitHub here.

NoSQL/nonrelational databases

This is slightly different from relational databases, with nonrelational databases, data is stored in a nontabular form. Storage is based on a structured document-like format and can handle detailed information while storing a wide variety of data formats.

When it comes to the NoSQL databases in Flutter there are a few and very promising options to consider, and the most popular of them is the Google Firebase, which is an online leverages on cloud storage and we also have other user tailored options such as Objectbox, Hive, and SharedPreferences.

Firebase

To learn more detailed information about Firebase and also get your hands on their codelabs, you can head over to the FlutterFire Overview page here.

One of the major perks of using Firebase that I personally love is the fact that storage location is cloud based, and this means we can synchronize data across multiple devices, instead of saving them to a user’s specific device.

Firebase provides more than one option to store data: we have Firebase Storage, Firebase Firestore, and the Realtime Database. Each of these can be chosen based on your use case and the type of data to be stored. For simple document storage, the Firebase Firestore works very well.

Firebase also has a free plan, and most other advanced features would need to be paid for, but overall, Firebase is great and fairly easy to integrate.

Hive

Hive is a lightweight and blazing fast key-value database written in pure Dart.

Hive is one of the storage plugins with the highest number of likes on the pub.dev website, and the reason a lot of people love it is of how easy it is to use.

Here’s an example snippet of how to set it up and begin using it immediately in your project:

var box = Hive.box('myBox');
box.put('name', 'David');
var name = box.get('name');
print('Name: $name');

Looks too easy, right? Well, that is why it is one of the most widely used in the Flutter community.
Aside from storing key-value pairs, Hive can also be used to store objects:

@HiveType(typeId: 0)
class Person extends HiveObject {
  @HiveField(0)
  String name;
  @HiveField(1)
  int age;
}
var box = await Hive.openBox('myBox');
var person = Person()
  ..name = 'Dave'
  ..age = 22;
box.add(person);
print(box.getAt(0)); // Dave - 22
person.age = 30;
person.save();

To get more info, check out the pub.dev package page or Hive docs, and if you encounter any issues, be sure to open an issue on the repository issues page, or check to see if someone has had such an issue before now.

ObjectBox

ObjectBox is a super fast database for storing objects locally in Flutter.

It has some good features, just like a vast majority of others, some of which include, scalability, being statically typed, being multiplatform (which means it can run on Android, iOS, web, and desktop), and performing well with memory.

To get more information on how to use and implement ObjectBox for local storage in your application, check out the official tutorial here.

shared_preferences

shared_preferences is one of the most common ways mobile developers store key-value pairs locally on their application, and this is a relatively easier and faster option.

The only drawback of using shared_preferences is that it is not advisable for storing large chunks of data, and lists.

In order to use shared_preferences in Flutter, you simply add the dependencies and run the Flutter pub get command. After that, you create an instance of SharedPreferences and await it, since it returns a Future.x

After that, you use the variable type callbacks that the instance provides to save it, and use a similar callback to retrieve your data when needed. This would look something like this:

Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
_prefs.setInt("counter", 1);
var result = _prefs.getInt("counter");

For more detailed information, check out the pub.dev page here.

Final Thoughts

Flutter is increasing in popularity, but there are not so many options for storing data. However, the packages available can serve your need notwithstanding. The above guide has shown us some options and key points for consideration.

For instance, if you needed to store data and provide sync across different devices, you would have to go with Firebase, and if you are not going to be storing data continuously, Hive or shared_preferences might seem like a good choice.

It all boils down to your use case and application needs.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Eshiet Ekemini A graduate of University of Uyo and a tech enthusiast, Ekemini has been building for mobile for two years, with a particular focus on Kotlin and Flutter.

Leave a Reply