• Rayhan Memon
  • Posts
  • #21 - A Reactive, Local-First Mobile Application in Flutter

#21 - A Reactive, Local-First Mobile Application in Flutter

So my team and I finally put out the Alpha of our mobile app for our team to play around with it. And. It. SUCKS!

But that’s ok… You have to do something poorly before you can do it well. Going in, you aren’t aware of all the ways your architecture is going to fail you. Here’s some ways our current architecture is failing us:

  • It’s not clear where state should live. Like the user’s authentication state, the currently rendered posts, the group they have open, etc.

  • There are criss-crossing dependencies between various services in our app, making it complicated to avoid cyclic dependencies or even know which ones to initialize first.

  • Views in our presentation layer are sometimes using high-level services and other times using low level storage APIs directly.

  • Modules throughout our app are reacting to state in different types of ways. Some are polling values, others are listening to event streams. Some aren’t reacting at all…

  • We aren’t listening for updates from our remote database and haven’t established a pattern for doing so.

  • We hand-waved the local storage side of the app so the user experience is more or less the same offline as it is online.

This is no way to live.

So I took some Adderall, put the NFL playoffs on TV, and outlined what I’m humbly calling “The Perfect Reactive, Local-First Mobile Application”. Here it is:

We’re extending Flutter’s “Stacked” framework (which I couldn’t recommend more).

The goal with this architecture is to make it blindingly obvious to current and future engineers where to position new code to extend functionality and where to discover existing code for current functionality.

Let’s walk through the separation of concerns for each module.

Providers

Classes that provide functionality that is broadly useful throughout the application. Examples include:

  • Connectivity Provider – indicates whether or not the device is connected to the internet.

  • Local Storage Provider – interface for storing and retrieving files from local storage.

  • Local Database Provider – interface for storing and retrieving data from the local database.

Domain Models

The data structures used throughout the flutter application. They have no outside dependencies and therefore do not implement methods like toDB or fromDB which would couple them to Database Models. The reason for this choice is that Domain Models don’t always match up one-to-one with Database Models (see below). Some Domain models are composed of multiple.

Remote DB Models

The data structures stored in our remote database. There is one remote DB model for each public table and view in our database.

Local DB Models

The data structures cached client-side. These should match up almost exactly with our remote DB models. The only differences should be:

  • Special properties required for caching

  • Altered data types for certain properties due to incompatibility. (E.g. a datetime being stored as string).

Remote Data Sources

Classes that expose a simple CRUD interface for remote database objects. There’s one for each database table or view that is read from. Data Sources can correspond to one or more remote DB models depending on if they are reading from a view. These Data Sources also listen for and relay events on the tables/view they correspond to. 

Duties:

  • Coordinates reading from table/view and writing/updating one or more tables (several tables are updated if reading from a view).   

  • Real-time listening and emitting of events to their table.

Depends On:

  • Remote DB Models

  • App State

Local Data Sources

Classes that expose a simple CRUD interface for local database objects.There should be a matching local data source for every remote data source to make coordination by the ‘Repository’ straightforward to manage.

Duties:

  • Creating and responding to cache specific properties

  • Cache invalidation

Depends On

  • Local DB Models

  • Local Database Provider

Repositories

Classes that implement a simple CRUD interface for Domain Models. There is one per Domain Model. This is a major abstraction that does a lot of heavy lifting.

Duties:

  • Coordinate multiple Data Sources to execute CRUD operations against Domain Models correctly.

  • Toggle between local and remote data sources based on connectivity.

  • Sync local and remote data sources in the background.

  • Receive and relay events from the Remote Data Source to the Event Bus for real-time reactivity.

  • Encrypt/Decrypy

Depends On:

  • Remote Data Sources

  • Local Data Sources

  • Local DB Models

  • Remote DB Models

  • Connectivity Provider

  • App State

  • Event Bus

  • Secure Storage

Note: The filesRepository and its accompanying data sources are a special case that deal strictly with blob data stored in a remote bucket or cached locally. These will mostly follow the same pattern.

Services

Classes that contain reusable business logic for view models and the Event Manager to take advantage of. Uses the simple CRUD interfaces presented by repositories to execute complex queries.

These services have minimal state and DO NOT subscribe to events on the event bus or repositories. When called directly, they use getters to fetch the state they need from the App State.

Services do not update App State directly. If state pertains only to this service, it is contained within the service itself. If state is reused across multiple services, an event is emitted with new values that the Event Manager will use to write.

Duties:

  • Provide reusable business logic for viewmodels and the event manager.

  • Emit events to the event bus when other parts of the application should react.

  • Only when called directly by a view model or the event manager, get necessary state from App State.

Depends On:

  • Event Bus

  • Repositories

  • App State

Event Bus

A simple pub/sub system that repositories and services can emit events to. These are events that other parts of the application need to react to. All events are handled by the event manager.

Some examples of events that should hit the event bus are:

  • User logged out

  • A friend made a new post.

  • A new member joined the user’s group

  • Someone edited their reply

  • etc.

Event Manager

Listens to events from the event bus and acts accordingly to coordinate different services and update the app state. This is the only module in the application that listens and reacts to events on the event bus.

This service exists to avoid confusion about which service/repository should be updating App State and handling events.

Duties:

  • Handle all events emitted to the Event Bus

  • Writes to the App State.

Depends On:

  • Event Bus

  • Services

  • App State

App State

Stores application state that other modules in the application rely on.

The App State implements the ListenableServiceMixin so that viewmodels (and only viewmodels) can listen for and react to changes in the App State. For example, if the user changes the currentGroup, the homeview should re-render

Note: The App State is read only for repositories and services. The only module that updates the state is the Event Manager.

View Models

Manage the state of a view. Reacts to changes in the App State

Duties:

  • Listen to changes in App State for relevant state and updates accordingly.

  • Leverages services for complex business logic

Depends On:

  • App State

  • Services

And there you have it. I’ll update y’all on whether or not this architecture is indeed “perfect”. Fingers-crossed it is 🤞

Quick reminder - If you appreciate my writing, please reply to this email or “add to address book”. These positive signals help my emails land in your inbox.

If you don't want these emails, you can unsubscribe below. If you were sent this email and want more, you can subscribe here.

See you next week — Rayhan