There are a lot of options out there today when it comes to databases in your application. They typically fit into these three categories:
- Relational - these are the databases in the traditional sense. They don’t just store data but also the relationships between the data. SQLite is an example of a relational database.
- NoSQL - these databases store data as documents. A schema is not enforced as is the case with a relational database. They are lightning-quick and handle huge unstructured pieces of data very well. MongoDB is an example of a NoSQL database.
- Individually tailored data storage - while this option is not technically a database, you don’t have to use the above solutions. You can store your data in a JSON file and handle the serialisation and deserialisation yourself. This would be incredibly fast but would open you up to some weird bugs if you weren’t a programming genius. 🤓
By the end of this article, you will know:
- The basics of how each database works. 📰
- How to get set up with each database. ⚙
- What a good use case would be for each database. 📳
In this article, we’ll divide our database types according to the three categories mentioned above.
Relational
Relational databases have been around for a very long time (since 1970, according to a quick Google search). Let’s look at some of the options you have on Flutter for relational databases today.
SQflite
SQflite is an implementation of SQLite for Flutter. It affords you complete control over your database, queries, relationships, everything you could wish for.
Interacting with a SQLite database in Flutter looks like this (from the docs):
// 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)');
});
Inserting data follows the same age-old SQLite tenants
// 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');
});
And querying happens like this:
// Get the records
List<Map> list = await database.rawQuery('SELECT * FROM Test');
Pros 👍
- Total control over the database.
- A very efficient SQLite database for your app (given that you have pre-existing SQLite knowledge).
- Easily read by third party apps that can open SQLite databases (so you can open the database on your computer and see what the data looks like).
Cons 👎
- Writing out all your queries by hand can take a lot of time.
- Returned data from the database isn’t strongly typed from the start.
- It is potentially difficult to handle migrations on the device (when the schema changes between version updates for instance).
- It has no support for web.
Use case
SQFlite is good when you need relational data but also fine-grained control over the actual queries. If you’re comfortable with writing your own queries and don’t mind writing both a lot of queries and the code to convert the results to and from your classes, this could be a good fit for you.
SQLite abstractions
Directly using SQLite to manage your application database can be quite powerful but also unwieldy. That’s why there are so many solutions that abstract some of the functionality from SQLite into more easily used functionality. These abstractions can make a SQLite database easier to use, while still retaining a lot of the benefits of SQLite.
Floor and Moor are fairly popular examples of this approach. In this article we’ll look at Moor, but the approach that these two packages take in abstracting SQLite is fairly similar.
Moor
In order to use Moor, we import the Moor package from flutter pub, but we also have to import something called the moor_generator
. This is used by build_runner
to generate the code to use the database.
Why do we use
build_runner
?build_runner
is primarily used to generate code for your Flutter projects. Before I came to Flutter, I rarely, if ever, had to use a code generation utility. The main reason for this is because most other languages that I had used up until this point (such as C#) supported reflection.Simply put, this makes it possible for the framework to dynamically invoke parts of the program at runtime. It’s quite powerful, but typically it can be quite slow. It also affects linking of the produced application, as with reflection, technically every part of your application could be accessed or used.
When packages use functionality that typically were provided by reflection, they usually use
build_runner
to generate the necessary code ahead of time. This results in faster code execution at runtime, and also results in better ‘tree shaking’, or minimisation of the application binary at deployment time.
A look into the Getting Started documentation helps us understand the way a database is created.
import 'package:moor/moor.dart';
// assuming that your file is called filename.dart. This will give an error at first,
// but it's needed for moor to know about the generated code
part 'filename.g.dart';
// this will generate a table called "todos" for us. The rows of that table will
// be represented by a class called "Todo".
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 6, max: 32)();
TextColumn get content => text().named('body')();
IntColumn get category => integer().nullable()();
}
// This will make moor generate a class called "Category" to represent a row in this table.
// By default, "Categorie" would have been used because it strips away the trailing "s"
// in the table name.
@DataClassName("Category")
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get description => text()();
}
// this annotation tells moor to prepare a database class that uses both tables
// we just defined. We'll see how to use that database class in a moment.
@UseMoor(tables: [Todos, Categories])
class MyDatabase {
}
Moor programatically creates the schema for your tables depending on how you define their contents. In the beginning of this code example, we can see the part
statement. When we run the build_runner
command, moor
generates the schema based on what we have defined in this file. You can also drop back to raw SQL at any time if you need to run a specific query or if you want more fine-grained control.
Pros 👍
- Strongly typed results!
- Based on SQLite.
- You don’t have to manually construct every query by hand.
- A lot of the heavy lifting is handled by code generation.
- SQLite database can be navigated with a wide range of tools available today to check data during development.
Cons 👎
- It can be cumbersome to handle schema updates on the local device.
- Web support is still in preview.
- It has platform-specific dependencies (not written in pure Dart).
Use case
If you still need relational data but want to write as little SQL as possible (if you are used to Entity Framework, for instance), this could be a good fit for you.
NoSQL
There are quite a few options when it comes to NoSQL databases for Flutter as well. We have the heavy hitters that have been around for a long time like Firebase, as well as the newer solutions like Hive. There are many differences between Hive and Firebase but perhaps the main difference is that one can sync to an online store (Firebase), whereas the other is more adequately suited for storing locally on the device (Hive).
Firebase - online NoSQL storage
Firebase is a traditional document storage database. You store data in collections that are like tables in a traditional database. These collections store documents. Documents store data types, like string
, int
, etc. They can also store a link to another document, so even though Firebase isn’t strictly relational you can still create relationships between your data.
The setup for Firebase is quite involved compared to other on-device options, like Moor or Hive, but you get synchronisation of data between clients and the server. This means that if you have multiple clients with an app and they are all interacting with the same data, then this data can be kept in sync between these clients. Also, this setup is covered quite well in a Google Codelab here. The only downside of this approach is that you don’t get strongly typed data in the same way as you do with Moor or Hive. You have to handle this yourself.
Pros 👍
- Synchronises with Firebase online in a near real-time fashion.
- Great tooling support.
- Easy to browse data online via the Firebase Console.
Cons 👎
- Firebase setup can be complicated if you don’t already have it added to your app.
- As the database is online, you need to be mindful of a lot more than a strictly on-device database (like access permissions, for instance).
- Firebase support for Flutter isn’t in a production ready state just yet.
Use Case
If your data is to be spread between many devices and you want (relatively) painless synchronisation between these devices, this could be a good solution for you.
Hive - offline NoSQL storage
Hive is an extremely fast NoSQL storage option for Flutter developers. Its biggest selling point is that it is completely native to Dart. This means that anywhere Dart goes, Hive can go, as it doesn’t require any device-specific implementations.
Hive lets you store data as a HiveObject
, which allows for some relations between objects as well. It stores objects in a box
, but you can generate TypeAdapter
s for them.
Creating a box
is fairly simple:
var box = await Hive.openBox('testBox');
Reading and writing is just as easy:
import 'package:hive/hive.dart';
void main() async {
var box = await Hive.openBox('testBox');
box.put('name', 'David');
print('Name: ${box.get('name')}');
}
Where Hive really shines is in the ability to generate TypeAdapter
s (read more from the documentation). This brings strong typing to your Boxes, and lets you store your classes, and also lets you reference objects in other Boxes.
The documentation shows how to create a box
based on a class with your own definition, like below.
import 'package:hive/hive.dart';
part 'person.g.dart';
@HiveType()
class Person
{
@HiveField(0) String name;
@HiveField(1) int age;
@HiveField(2) List<Person> friends;
}
As we can see here, this class contains a List
of Person
, so Hive can reference a list of objects.
Use case
If you are just after a simple database to store data on your device, and don’t want the synchronisation that Firebase offers and if you want something that works anywhere, Hive is for you. It’s the database I use in all my apps.
Making your own
If you’re not smitten with any of the above options, you could roll your own database. You would achieve this through using a library like json_serializable
, and storing the JSON files on the device for the app to access.
In my opinion, this would be a little bit like building your own car from scratch. Of course, you can do it, and people have done it, but I’m not sure why you would. Creating a new database means creating a new library with potential bugs and so many new issues to consider and think about. If you have set out to make an app, is creating your own database solution really within the scope of doing that?
Existing solutions like Hive or Moor are already in use by a lot of people, all finding bugs and filing issues on GitHub. This is what gives these libraries quality. Who would find bugs on your brand-new, from scratch database? Because you’re the only person that uses it, only you would. That doesn’t sound very appealing does it? I hope not.
Use case
If you completely distrust all code not written by you, or you have some weird esoteric use case, you would maybe consider making your own. I wouldn’t consider doing this in any app I would make though …
So many options! Which one should I use?
There’s not an easy way to answer what database is “best” - it depends on your use case. But let me sum it up.
- If your data is relational and you want to be able to view it on your computer easily during development - and you don’t mind not having web support - you should use moor.
- If you need your data synchronized between devices and you don’t mind the fairly extended setup, you should use Firebase.
- If you want to be up and running really quickly and want great support for the web plus anything else Dart runs on, you should use Hive.
- If you have crippling doubt about the security of these databases and nobody can convince you otherwise, and you have a lot of time on your hands, you could consider making your own database or object store with JSON objects. But I wouldn’t.
If I was making an app today, I would probably use Hive.
Thanks for reading this article on databases for Flutter. I hope it has made choosing one a little easier for you!
Lewis Cianci is a software developer in Brisbane, Australia. His first computer had a tape drive. He’s been developing software for at least ten years, and has used quite a few mobile development frameworks (like Ionic and Xamarin Forms) in his time. After converting to Flutter, though, he’s never going back. You can reach him at his blog, read about other non-fluttery things at Medium, or maybe catch a glimpse of him at your nearest and most fanciest coffee shop with him and his dear wife.