How Does a Flutter App Work Offline, and How Is Data Synchronized?
Yusuf İhsan Görgel · Published: 10.10.2026 · Mobile app (iOS / Android)
A field worker is filling out a form in a basement, a store employee is counting products, or a warehouse worker is checking shelves. The connection can drop at any moment. If the app only opens or completes a record when it can reach the server, the work is left unfinished. To design a Flutter app that remains usable without internet, you first need to decide where the data will be stored and how it will be transferred once the connection is restored.
Offline support does not mean adding an error message to every screen. You need answers to questions such as which tasks users can perform without a connection, which information must be up to date, and what happens if the same change is also made from another device. These decisions come before the UI code. Small, clear rules reduce data loss and inconsistency problems that surface later. I work on the local data layer and synchronization of a point-of-sale app that must keep selling even when the internet goes down; in this article I explain the key decisions that need to be made in apps like this.
Make the local database the app's working source
Drift is a layer used to access SQLite databases in Flutter and Dart projects. It helps you define data as tables, write queries, and observe query results as streams. Because the SQLite database lives on the device, reading and writing records does not depend on a network connection.
Rather than having screens display data directly from an HTTP response, a more robust setup is to feed them from the local database. New or changed records coming from the server are first written to SQLite. The screen then shows the current state of the database. This way, the UI uses the same data path whether there is internet or not. When the app opens, the stored content appears first, and once synchronization completes, changes are reflected on screen automatically.
Drift's watch() queries emit a new stream of results when the watched tables change. For example, a screen observing the product list can be redrawn when the sync service updates a product's stock quantity. This structure removes the need for screens to initiate network requests. The network layer updates the database, while the UI listens to the local query. This separation makes offline behavior easier to understand and debug.
Making the local database the single source of truth does not mean the server is unimportant. The server is where persistent, shared state across users and devices is kept. However, the live working state the app displays on screen is read from SQLite. Data received from the server is applied to local records; user changes are also saved locally first, then sent to the server. This way the screen is never empty during a network request, and a save is not lost because of a connection error.
Keep pending operations in an outbox table
When a user completes a task while offline, the app should update the local record immediately. For the same operation, the command to be sent is also written to a pending operations table called the outbox. Each row can hold the operation ID, the payload to send, the creation time, the attempt count, and the status. Sensitive information should not be kept in the queue unnecessarily, and records should be cleaned up according to the retention policy once the operation completes.
The sync service can pick up pending rows in order when the app starts, when the connection returns, or at regular intervals. Each operation is marked as completed once it is sent successfully. If an error occurs, it stays in the queue and is retried later. For example, a customer record must be created first, and only then can an order linked to that customer be sent. That is why the dependency order of operations must be explicit in the data model. If the app closes during sending, the operation must not be lost; it should resume from the queue on the next launch.
Retries should not treat every error the same way. A temporary network drop or a brief server outage is suitable for retrying. An authorization error, an invalid field, or a deleted account, however, may require user or app intervention. To avoid sending frequent, back-to-back requests, an increasing wait time can be applied between attempts. When an operation keeps failing, show the user a clear sync status.
A network request may be processed on the server while the response never reaches the device. If the app assumes it failed and sends the same request again, an order or payment record may be created twice. Every outbox operation should carry a fixed, unique idempotency key. If the server has already processed that key, it returns the previous result without creating a new record. This protection makes client-side retries safe. The key must not be regenerated on each attempt; it should remain the same for the entire logical operation.
Choose the conflict rule based on what the data means
Two devices can modify the same record while offline. Which value prevails once the connection is restored must be defined in advance. With the last-write-wins approach, the update with the newer timestamp is kept. This can be practical for fields such as draft notes or a user's latest preference, but it carries risks: device clocks may differ, or an important update may be overwritten.
With the version number approach, the client sends the version of the record it saw when making its change. If the version has not changed, the server accepts the update and issues a new version. If another device modified the record first, the server returns a conflict response. The app can then download the current record and offer the user a choice, or merge it again according to business rules. This check is better suited to data where a wrong update has business consequences, such as stock, reservations, and financial records.
Field-level merging can preserve changes made by each side to different fields. For example, if one device changed the description and the other changed the delivery note, both can be kept. If the same field was changed on both sides, a decision is still needed. Merge rules should be designed for each data type rather than blindly combining all records.
Don't mistake a connectivity signal for an internet check
connectivity_plus can report whether the device is connected to a network such as Wi-Fi or cellular. This signal does not guarantee that the server is reachable. The Wi-Fi router may have no internet access, a corporate network may block the server, or the server may be temporarily down. So a connectivity change should be treated as a hint to attempt synchronization, not as proof of success. Timeouts, server errors, and retry behavior for real requests must also be handled.
Before you start, list the screens that will be used offline, the fields to be stored, and the decision to be made in case of a conflict. Then design the local tables, outbox statuses, and sync rules based on that list; test the app in airplane mode and in an environment where the connection drops frequently.
Flutter offline mode Drift SQLite synchronization mobile app outbox
This article reflects the expert's own views and experience.