[MUSIC PLAYING]
TJ DAHUNSI: Hi.
I'm TJ, a Developer Relations Engineer.
And today, I'm going to be talking about building
offline-first apps.
Why build an offline-first app, you ask?
Apps don't always have access to the internet.
But that doesn't mean they have to stop working.
So let's learn how to build one.
Building an offline-first app starts
in the data layer, the repositories, and the data
sources they delegate to.
An offline-first repository that has network access requires
a minimum of two data sources--
a local data source and a network data source.
It's the repository's responsibility
to populate the local data source with the network data
source.
Since the local data source and the network data source
often defend the implementations for fetching data,
it is often beneficial for each data source
to have a distinct model representing
the data it provides.
Consider a news app that has authors.
A local data source backed by SQL, for example,
may provide an author entity.
A network data source, however, may provide a network author.
The semantic differences between these two
is purely a data layer concern.
They should be kept internal to the data layer.
And the third model, author, should be
exposed to the rest of the app.
Next are the two actions that can be
performed on data in an app--
reading and writing.
Let's see how these are affected when going offline-first.
Reading an offline-first app should
be made available through observable APIs,
like Kotlin flows.
This is because data can change between connectivity states
and the rest of the app needs to be made aware of changes
as they occur.
When writing data, use asynchronous
APIs, like suspend functions from Kotlin coroutines.
This is because writes will cross a network boundary,
and we don't want to block the UI thread.
For both reads and writes, managing connectivity changes
requires two main concepts-- queuing
to the network as available, monitoring
to check network access.
Implementing the above follows in linear structure
where the implementation of each step varies.
For reads, this involves queuing the read,
waiting for a network connection,
performing the read from the network, and finally,
updating the local data source.
It is important to note the external layers of the app
must always read from the local data source.
This enforces data consistency in-between connectivity states.
For writes, the pattern is similar.
Write into the local data source first if the business logic
requires it, queuing the write to the network,
waiting for a network connection,
performing the write to the network, and finally,
resolving any conflicts that may arise after the write.
Writes in offline-first apps typically
require more consideration than reads because of conflicts.
Therefore, an offline-first app doesn't
need to be able to write data offline for it
to be considered offline-first.
If there's a failure in any of the above,
the solution typically is checking
if the operation should be retried, terminating
if it shouldn't, otherwise requeueing the operation to be
performed at a later time.
And finally, persist in the result when successful.
When connectivity is restored, an offline-first app
needs to reconcile data in its local data source
with that in the network data source.
This process is called synchronization.
The two main ways an app can synchronize with this network
data source--
pull-based synchronization and push-based synchronization.
In pull-based synchronization, the app
reaches out to the network to read the latest application
data on-demand.
A common heuristic for this is navigation-based-- when
an app visits a screen, pulls data for just that screen,
then visits another screen and pulls data just
for that screen, too.
The pros of this is that it's relatively easy to implement.
Data that is not needed will never be fetched.
And then the cons of this are it's prone to heavy data
use when screens are revisited.
It doesn't scale well with relational data,
as dependent data has to be fetched on-demand.
In push-based synchronization, the local data source
tries to mimic a replica set of the network data source
to the best of its ability.
It proactively fetches an appropriate amount
of data on first startup to set a baseline, after which it
relies on notifications from the server to alert it
when that data is still.
The pros of this is the app can remain offline indefinitely
because the baseline has already been established.
There's minimum data use after initial sync.
And it works well for relational data.
The cons of this are versioning data
for conflict resolution is non-trivial.
And there could also be concurrency concerns
during synchronization, and it must
be supported by the network.
If the app writes data locally when
offline that is misaligned with the network data source,
a conflict has occurred.
And you must resolve that before synchronization can take place.
There are a lot of strategies that may be
used for conflict resolution.
However, a common and robust approach is last write wins.
In the timeline shown, both devices have been synchronized
and are offline.
A short while later, device A writes data locally.
A little while after, device B writes conflicting data
locally, too.
Device A comes back online and then syncs with the network,
writing its data to the network.
Device B also comes back online and syncs with the network,
overwriting the data written by device A. Device A is then
notified about the change and has its local data
updated to match that that was last written by device B.
To learn more about building offline-first apps,
be sure to check out the Now In Android
sample and the offline-first guide.
See you next time.
[MUSIC PLAYING]