DEV Community

loading...
Cover image for Persistent Local Database with ObjectBox on Flutter

Persistent Local Database with ObjectBox on Flutter

theimpulson profile image Aayush Gupta Updated on ・5 min read

There are a lot of persistent database solutions available for Flutter applications. One of them is ObjectBox which is a NoSQL-based high-performance-oriented database solution written in the native dart language. You can use local as well as server features for syncing data as you like.

In this article, I will show you how you can perform the basic operations i.e. inserting, deleting, updating, and querying the data using the objectbox package for your Flutter applications.

Dependencies

You need to add a dependency on objectbox, objectbox_flutter_libs, and path_provider packages. Moreover, you need to add dev_dependencies on build_runner and objectbox_generator packages.

dependencies:

  # ObjectBox
  objectbox: ^0.11.0
  objectbox_flutter_libs: any
  path_provider: ^1.6.27

dev_dependencies:

  # ObjectBox
  build_runner: ^1.0.0
  objectbox_generator: any
Enter fullscreen mode Exit fullscreen mode

These packages are required for these respective reasons:

  • objectbox to create and maintain the database,
  • objectbox_flutter_libs to provide flutter-runtime libraries for the objectbox,
  • path_provider to provide the path to the application's document directory to store the database,
  • build_runner to generate dart files for using our code, and
  • objectbox_generator will generate the files using build_runner.

Implementation

Model Class Implementation

In order to implement the database, we need to create a Model class annotated by the @Entity() annotation which will be used by objectbox to generate required bindings for us to use later.

We will create a new Person class to proceed with this example in a new directory inside lib/ called database.

Here is how lib/database/person.dart looks in my case:

import 'package:objectbox/objectbox.dart';

@Entity()
class Person {
  String firstName;
  String lastName;
  int age;
  int id;

  Person({this.firstName, this.lastName, this.age});
}

Enter fullscreen mode Exit fullscreen mode

You can notice that I have left the id parameter out of the class constructor. The reason for this is that objectbox will dynamically allocate a new id for the model class and we don't need to worry about manually incrementing it after every operation.

Once you have implemented your model class, run the build_runner so that the objectbox can generate the required binding files.

$ flutter pub run build_runner build
Enter fullscreen mode Exit fullscreen mode

The binding files will be created inside your lib/ directory. In case you use a version control system, make sure that you ignore the objectbox.g.dart file which will change every time you modify the class configuration.

Store Implementation

Now we can proceed to create an instance of the database. To do this, I will create a new class called BoxStore in a new file which will contain a single static method returning an instance of the Store object which is your database.

We will need to provide it the path to store the database which we will get using the getApplicationDocumentsDirectory method provided by the path_provider package.

Here is how lib/database/store_init.dart looks in my case:

import 'dart:io';

import 'package:objectbox_example/objectbox.g.dart';
import 'package:path_provider/path_provider.dart';

class BoxStore {
  static Future<Store> getStore() async {
    /// Returns an instance of the Store
    Directory dir = await getApplicationDocumentsDirectory();
    return Store(getObjectBoxModel(), directory: dir.path + '/objectbox');
  }
}

Enter fullscreen mode Exit fullscreen mode

You can notice that I am using the getObjectBoxModel function as an argument. This function is autogenerated by objectbox and is present in the binding files which we generated earlier.

You should follow the singleton design pattern when instantiating the Store as each instance is fairly expensive and one rarely needs access to multiple instances.

How do you build a Singleton in Dart?

284

The singleton pattern ensures only one instance of a class is ever created. How do I build this in Dart?

Store Methods Implementation

Now as our Store class is ready, we can use it to get an instance of our database and use various methods on it. To do that, I will create a new class in a new file named StoreHelpers and write various functions to perform the basic operations over our database.

Here is how my lib/database/store_helpers.dart looks like:

import 'package:objectbox_example/database/person.dart';
import 'package:objectbox_example/database/store_init.dart';

class StoreHelpers {
  /// Contains various static methods to help with the database
  /// management.

  static Future<int> insert(Person person) async {
    /// Inserts the given [person] object into the box
    var store = await BoxStore.getStore();
    var box = store.box<Person>();
    return box.put(person);
  }

  static Future<bool> delete(int id) async {
    /// Removes a person object from the box using the given [id]
    var store = await BoxStore.getStore();
    var box = store.box<Person>();
    return box.remove(id);
  }

  static Future<List<Map<dynamic, dynamic>>> queryAllPersons() async {
    /// Returns a List containing Map of the person objects stored in the box
    var store = await BoxStore.getStore();
    var box = store.box<Person>();

    var listToReturn = <Map<dynamic, dynamic>>[];
    var listOfPersons = box.getAll();

    for (Person person in listOfPersons) {
      var map = {};
      map['firstName'] = person.firstName;
      map['lastName'] = person.lastName;
      map['age'] = person.age;
      map['id'] = person.id;
      listToReturn.add(map);
    }
    return listToReturn;
  }

  static Future<Person> queryPerson(int id) async {
    /// Returns a person stored in the box using the given [id]
    var store = await BoxStore.getStore();
    var box = store.box<Person>();
    return box.get(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

In all of the above methods, you can notice that I create an instance of the store using BoxStore.getStore() and then call the box method over it with the Model Class's type. After that, I perform the respected operation using the box variable.

Here are the methods I am using for the operations:

  • put to insert a new object into the box using the given person as an argument,
  • remove to remove an object using the given id as an argument,
  • get to get a single person using the given id as an argument, and
  • getAll to get a List containing instances of Person which we recursively map to a new list and return it.

and with that, our backend implementation is now complete. We can start using it in our application to see it in action.

Consider destroying the Store instance when the app is finished.

Usage

I have created a simple application that has 4 buttons to perform the basic operations. I will be logging the response of the methods to see if it was successful or not.

The operation performed by the buttons is self-explanatory in my opinion and not the main focus of this article, hence I will skip explaining it.

Here is how my main.dart file looks like:

import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:objectbox_example/database/person.dart';
import 'package:objectbox_example/database/store_helpers.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              RaisedButton(
                child: Text('INSERT'),
                onPressed: () async {
                  var person =
                      Person(firstName: "Aayush", lastName: "Gupta", age: 22);
                  int id = await StoreHelpers.insert(person);
                  log("Sucessfully inserted an object with $id");
                },
              ),
              RaisedButton(
                child: Text('UPDATE'),
                onPressed: () async {
                  var person = await StoreHelpers.queryPerson(1);
                  person.firstName = 'Nidhun';
                  person.lastName = 'Balaji';
                  person.age = 22;
                  int id = await StoreHelpers.insert(person);
                  log("Sucessfully updated an object with $id");
                },
              ),
              RaisedButton(
                child: Text('DELETE'),
                onPressed: () async {
                  bool deleted = await StoreHelpers.delete(1);
                  if (deleted) {
                    log("Sucessfully deleted an object with id :1");
                  }
                },
              ),
              RaisedButton(
                child: Text('QUERY ALL'),
                onPressed: () async {
                  var list = await StoreHelpers.queryAllPersons();
                  log(list.toString());
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

and with that, now you can use objectbox to create and maintain a local database for your Flutter applications. Make sure to check the official documentation in order to see the complete API documentation and make the most of it.

Discussion (3)

pic
Editor guide
Collapse
ivan profile image
Ivan Dlugos

This started as a really nice roundup, though after looking at the main.dart example, I've been curious why you'd use those static "StoreHelpers".

I've had a look at the objectbox_example repo you've created in your github account and what you're doing is opening the store for every operation which is very inefficient. Besides, since you're never closing the store, it's also leaking memory on every call.

What were the reasons to go this approach, as opposed to keeping the store open during the app lifetime as in ObjectBox's example for Flutter?

Collapse
theimpulson profile image
Aayush Gupta Author

why you'd use those static "StoreHelpers"

Grouping data in a class that works on a similar object but as they don't depend upon each other they are static

What were the reasons to go this approach

The article is simply demonstrating how one can use the objectbox to work with a local persistent database. I do agree that I should have gone with a singleton pattern to create a Store instance or should have added a note for that. Just did now after seeing your comment.

Collapse
theimpulson profile image
Aayush Gupta Author

Ah, I get what you meant with static methods. Yeah, they don't need to be static considering the store and box variable is being repeatedly used, it can be moved to a class-level variable.