What we've learned and had to deal with while using ObjectBox as our database of choice
Initially, our app used the sqflite
package as our database (i.e. SQLite). The iMessage database on macOS is an SQLite database, so it made the most sense for our app to emulate the same structure (which it did, we copied the CREATE statements for each table and used them to build our own internal database).
This worked well for the most part, but we hit a major roadblock when we decided to port our app to desktop platforms using Flutter and the sqflite_common_ffi
package - The speeds decreased dramatically. A simple DB call could take upwards of 5-10 seconds, and this was causing significant delay for actions like loading chats, message threads, etc which should normally take up to 1 second.
As a result, we decided to look into alternative databases. We landed on two potential ones: sqlite3
and objectbox
. We first tried sqlite3
to avoid a huge migration of code, but it was the same issue - speeds were too slow on desktop. Then, we moved to objectbox
. The speed issue was resolved, but we discovered a whole host of other issues that required some hacky workarounds and advanced Dart coding to resolve, and this page will discuss that in depth.
It was very tricky to get our code to compile for web while using ObjectBox. The issue is that it uses some dart:io
exclusive code, such as Pointer
s and other things.
As a result, we need to create two separate files for the same model, with very slightly different code. Here's a quick example:
When importing this model into other files, you want to use the following import directive:
This ensures that when compiling for web platforms, it does not import ObjectBox libraries, which would prevent the app from compiling.
The same process can be used for any functions that call ObjectBox code, create an io
version which has the function actually perform a task, and the web
version is empty:
Then use a similar import directive as above, and if you wish, you can call this function on io
only by using if (!kIsWeb) openObjectbox();
in your code.
The other big issue with ObjectBox is a lack of an asynchronous API. If you use synchronous code for larger reads in your app, it could cause heavy jittering and lag on page load. This was a huge problem for us, but thankfully there are two ways to mitigate it.
Requires Flutter 2.5 (Dart 2.14) and higher!
Do not use this method if you plan to use ObjectBox Relations! It will not work!
The first thing to do is store a reference to the ObjectBox Store
object by encoding it into a base64 string:
You'll want to do this right after you initialize your Store
in the app.
Next, let's take an example class:
As you can see, we are trying to get chats from ObjectBox using a given limit and offset. We use the Flutter compute
function which handles all the boilerplate code for creating an isolate, sending, and receiving arguments.
getChatsIsolate
must be a top level function, and is defined like so:
We get the arguments from the list sent to the function. The most important part is to use the same Store
reference as in the main thread!
This code will get you the best performance possible in the app, and we highly recommend doing it this way. What if you must absolutely use relations though?
async_task
PackageWe only recommend using this method if you absolutely need to use ObjectBox Relations! This method is not as performant, but is still definitely better than using the synchronous API.
When using the async_task
package, the example shown above would look something like this:
And GetChatAttachments
is defined as follows:
Finally, we also must add the createAsyncTask
function:
The parallelism: 0
is very important here. It makes sure the async_task
package doesn't try to use an isolate (which would break due to the Relations), and instead use an async zone to run the task.
Oftentimes jank will happen on animations - you will see dropped frames or stutters. To solve this, there are some things you should check and try doing:
Use transactions to group larger reads / writes into a single block
Reduce the number of reads / writes by simplifying your code
Run database code after page animations are complete:
You may have a background isolate that runs Dart code when an external event happens, e.g. when you receive an FCM notification. This is the proper way to use the Store
inside your isolate:
Every time you initialize a new Store
, you should store the reference inside some sort of SharedPreferences, like so:
This ensures that your code retains concurrency if your app happens to be active but also receives a background event that starts the secondary isolate.