Engineering
February 9, 2024
February 12, 2024
(updated)

How Local-First Simplifies Flutter State Management

Phillip van der Merwe

Click-to-deploy backends like Supabase have made it much easier to get a Flutter project off the ground quickly. 

However, state management remains complicated. In Flutter, built-in widgets have limitations and the many different third-party packages (flutter_bloc, Provider, Riverpod, etc.) make it hard to decide which one to use. And implementing caching, invalidation and optimistic updates can be challenging even with a great state management framework.

But it is actually possible to eliminate most of the complexities of state management, with local-first architecture.

What is local-first?

Local-first is a development paradigm where your app code works directly with a client-side (local) database which syncs with a backend database in the background. This is in contrast to cloud-first apps which primarily use a cloud datastore via APIs.

Local-first simplifies state management because your local database is the primary data source for your app. The diagram below shows the difference in how data would travel from source to the UI:

Note: on the local-first side, the local database will typically be kept in sync with a backend database through a separate, asynchronous process.

In local-first apps, the global state is simply stored in the local database. This substantially simplifies application code since queries can be done synchronously (without a roundtrip to the backend) and the developer does not have to handle loading states or network failure states.

The app will be fast and responsive since there’s no need to wait for network requests.

What about sync?

Unless you’re building a local-only app (which is a valid way to get your project off the ground quickly), a way to sync data between the local database and the backend database will be necessary. 

Once you have a local database that is always in sync, state management becomes much easier:

  • No need for custom caching logic, whether in-memory or persisted.
  • No need for maintaining in-memory state across the application.

All state is in the local database. Queries are reactive — updating whenever the underlying data changes.

Local-first provides a simpler paradigm for app development, allowing you to keep your logic simple and functional. As the Project Lightspeed team at Meta said about the benefits of a SQLite-driven architecture: “We leveraged the SQLite database as a universal system to support all the features. [...] The UI merely reflects the tables in the [local] database.” 

How to build local-first

Developing local-first is simple in theory:

  1. Implement a local database as part of your app.
  2. Implement a way to transfer data updates between the local database and the backend database (a sync system).

In reality this is not so easy. For one thing, there are a few local datastore options available for Flutter, and choosing one can be just as challenging as choosing a state management solution. For another, implementing a sync system usually becomes a rabbit hole for anything but the most simple apps.

@EvHaus on X / Twitter

We built PowerSync to solve these issues:

  • Our Flutter SDK includes a high-performance SQLite database: a proven relational database (SQLite is the most deployed database in the world).
  • The PowerSync service is a click-to-deploy sync system that can handle even the most complex sync requirements through a system called sync rules.

The PowerSync service is non-invasive to your backend: it can easily be added or removed from your stack.

Supabase setup

Using PowerSync with Supabase is probably the easiest way to go from zero to full local-first app with sync. We've written a Flutter tutorial as a step-by-step guide that takes around 40min to complete.

Trying local-first is easy with our free plan (sign up here), or see what others are building on our community Discord.

Happy coding!