Skip to content
Tech

Building Offline-First Flutter Apps: Local Storage and Sync Basics

Patchy mobile networks are a fact of life. Here is how to design a Flutter app that works offline, stores data locally and syncs reliably later.

Editorial illustration of a smartphone showing a cloud icon with a dotted connection line to a small local database cylinder
Editorial illustration of a smartphone showing a cloud icon with a dotted connection line to a small local database cylinder
In this article

    Users open apps in lifts, on trains, in basements and in towns where the signal drops to nothing. An app that shows a spinner forever in those moments feels broken. An offline-first design treats the local device as the primary source of data and the server as something you sync with when possible.

    What offline-first actually means

    In an offline-first app, every screen reads from local storage. User actions write locally first and the interface updates immediately. A background process then sends changes to the server and pulls down updates. The network becomes an enhancement rather than a requirement.

    Choosing local storage

    • Key-value stores such as shared preferences suit small settings and flags, not lists of records.
    • SQLite-based packages such as sqflite or drift suit relational data, queries and larger datasets. Drift adds type-safe queries and reactive streams.
    • Object databases such as Isar or Hive offer fast local storage with a simpler API for object-shaped data.

    For most business apps with lists, filters and relationships, a SQLite-based option is a dependable choice.

    Make the UI react to the local database

    Expose your data as streams from the local database, and let widgets rebuild when the stream emits. Combined with a state management approach you already use, this keeps the UI simple: it never cares whether data arrived from the server or from the user.

    Stream<List<Task>> watchTasks() =>
        (select(tasks)..orderBy([(t) => OrderingTerm.desc(t.updatedAt)]))
            .watch();

    Queue changes for sync

    When a user creates, edits or deletes something, write the change to the local table and add an entry to an outbox table describing the operation. A sync service processes the outbox in order whenever connectivity is available, removing entries once the server confirms them.

    • Give every record a client-generated ID, such as a UUID, so it can exist before the server sees it.
    • Store a sync status on each record: pending, synced or failed.
    • Make server endpoints idempotent so a retried request does not create duplicates.

    Pulling updates from the server

    Rather than downloading everything each time, ask the server for records changed since your last successful sync, using an updated_at timestamp or a server-issued sync token. Remember deletions too: the server needs to report removed records, often through a soft-delete flag.

    Handling conflicts

    Conflicts happen when the same record changes on two devices before they sync. Decide on a strategy up front.

    • Last write wins is simple and acceptable for many personal apps.
    • Field-level merging keeps non-overlapping edits from both sides.
    • User resolution shows both versions and asks, which suits important documents.

    Detecting connectivity sensibly

    Connectivity packages tell you whether the device has a network interface, not whether your server is reachable. Treat them as a hint to try syncing, and rely on actual request success or failure. Use exponential backoff for retries so you do not drain the battery.

    Be honest with users

    Show a subtle indicator when the app is offline and mark items that have not synced yet. Clear feedback builds trust; silently losing data destroys it. If a sync fails permanently, explain what happened and offer a way to retry or edit.

    Test offline paths deliberately

    Use airplane mode, network throttling and an emulator with the connection toggled mid-request. Write tests for the sync service with a fake server that fails at random. Offline bugs rarely appear on a fast office Wi-Fi connection.

    Start small

    You do not need to make every feature offline-capable on day one. Begin with the screens users rely on most, such as viewing recent records and creating new ones, and leave rarely used admin features online-only. Learn from how the sync behaves in real conditions, then extend the approach gradually.

    Wrapping up

    Offline-first takes more planning than a simple fetch-and-display app, but it produces software that feels fast and reliable everywhere. Read from local storage, queue writes in an outbox, sync incrementally, choose a conflict strategy and keep users informed.

    Written by Raj

    Founder & Editor

    I write HorizonDesk: practical, no-hype articles on technology, money, business and the vehicles we drive. Everything here is something I’ve researched, tested or wanted explained clearly myself.