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.
This section aims to provide extensive documentation and how-tos for all of the complex things we have managed to implement while working within the constraints of a Flutter app. We hope that this can be a useful guide / tool for other developers working on Flutter apps, especially Flutter-based chat apps.
Check the sidebar for all the documentation! (WIP)
As mentioned in , we chose Flutter over building something in React Native, or in Java itself. When deciding our framework, we never had full cross platform in mind, and rather we were focusing on the ease of development. While native Android would definitely provide the best overall user experience, we decided to make a small tradeoff on this aspect to utilize Flutter's ease of use, scalability, and quick turnaround times.
By far and away, the best thing about Flutter has to be the ease of development. Flutter layout design and UI is ridiculously easy to understand, and creating great UI is a simple task. We also really like Dart syntax, it makes code very readable and has a lot of great features to make coding easier.
Pub has been a great package repo - There are so many high quality packages available to drop into your project and eliminate the need for a ton of boilerplate code. We use everything from small UI packages to larger state / framework / dependency management packages.
There is definitely much more to like, but it isn't important to list all that here.
We would like to preface this section by saying we are not the most experienced developers in Flutter. Many of the issues listed here might be resolved with advanced code, but so far we haven't been able to find workarounds for them.
One of the biggest issues we face with Flutter is performance and memory usage. Before Flutter 2.8, our app would often surpass 2 GB memory when loading up more than 10-15 JPEGs or MP4s. This functionality is crucial for a messaging app where multimedia is a high priority. We also had many issues with lag in our transitions, animations, and system events such as the keyboard going up and down. Most of the performance issues were resolved with painstaking optimizations in code and the Flutter 2.8 upgrade, however.
Another large issue in Flutter is text input. Out of the box, Flutter doesn't support any sort of image / multimedia paste into textfields. This also means users can't insert GIFs or screenshots from Gboard on Android. To solve this, we ended up creating a fork of the Flutter SDK and engine, which we discuss in more detail at . For Web, we ended up using a Dart / JS interop function which is also discussed in the same section.
Our final major downside is just how Flutter interfaces with native. Things like creating notifications, supporting features such as conversation bubbles, or using the default copy/paste menu, are difficult to implement or simply not possible at this time. Many of these issues have open tickets on GitHub but they are either P3, P4, or P5 which doesn't bode well for having any chance these features are added in the near future.
With all that said, we believe the pros vastly outweigh the cons. Flutter has been a godsend for creating beautiful UI easily, and Dart is a very easy language with great tools to create mobile apps. The package support is also excellent, we love being able to just drop in packages and quickly add new features using them. Above all else, Flutter allowed us to extend not only to mobile, but also Web and Desktop with very minimal changes to our initial mobile-only codebase.
If you are creating a chat app specifically, keep the cons in mind. Be prepared to have to jump through hoops to integrate some important functionalities - however in the end it will be worth it.
How we added the ability to insert images via keyboard or copy/paste into Flutter
One of the most crucial requirements for a chat app is to allow users to insert rich content directly from their keyboard. On Android, this can be things such as stickers, GIFs, bitmoji, and even AI-based context-aware items (e.g. the keyboard offering to paste a recent screenshot). This functionality is sorely missing in Flutter, and we were forced to modify the Flutter Engine and Framework ourselves to implement it.
The basic content commit API is described at https://developer.android.com/guide/topics/text/image-keyboard. The Java code required to interface with Flutter's `TextInputClient` was added in the Flutter Engine.
Our PR to the Flutter Engine has been merged! The code essentially attaches the TextInputChannel
to the contentCommit
API, and sends the file data through the PlatformChannel
back to the Flutter Framework.
Our PR to the Flutter Framework is currently pending review. The framework code organizes the data received from the PlatformChannel
into a Dart class, and then this data can be manipulated however the developer chooses.
Flutter currently does not have any way to detect image content when a paste action is performed on a text field. We implemented this with the great interoperability that Dart and JS have. We created a simple function that uses the JS
package to call JS code from Dart:
This is called when we detect a CTRL-V event on the text field using a RawKeyboardListener
. The JS function unfortunately only works in Chrome, but it uses the browser Clipboard
APIs to get any media content:
This function is placed inside the web/index.html
file as a <script>
.
We use the pasteboard package on Desktop to get the most recent item (image) from the clipboard. This is also called when we detect a CTRL-V event on the text field using a RawKeyboardListener
.