How to Build an Offline-First Flutter App: Architecture, SQLite Caching & Sync
Offline-first Flutter app guide: SQLite caching, delta sync, conflict resolution, and lessons from a navigation app that works 100% without internet.
Why Offline-First Matters
Most mobile apps assume connectivity. Show a spinner, wait for the API, display the data. This works on fast Wi-Fi. It fails in underground car parks, aeroplanes, rural areas, and the dozens of dead zones that every mobile user navigates daily.
Offline-first means your app works completely without internet. Data loads instantly from local storage. User actions are captured locally and synced when connectivity returns. Network becomes an enhancement, not a dependency.
We built this architecture for TrailMaps Co., a navigation app for outdoor adventurers. Their users are specifically in areas with no signal. Here's exactly how we did it.
The Core Architecture
An offline-first Flutter app has four layers:
- Local database (SQLite via sqflite), the source of truth for the UI
- Sync engine, responsible for pushing local changes to the API and pulling remote changes locally
- Connectivity monitor, detects online/offline transitions and triggers sync
- Conflict resolver, handles cases where the same data was modified both locally and remotely
SQLite Schema Design for Offline
The key insight is that every table needs three extra columns:
created_at_local INTEGER -- epoch ms, set when record is created offline updated_at_local INTEGER -- epoch ms, updated on every local change sync_status TEXT -- 'synced' | 'pending' | 'conflict'
When the user makes a change offline, you write to SQLite immediately (so the UI responds instantly) and set sync_status = 'pending'. When connectivity returns, the sync engine picks up all pending records and pushes them to the API.
Delta Sync: Only Transfer What Changed
Full data sync, downloading everything on every connection, is wasteful and slow. Delta sync downloads only records that changed since the last sync.
The implementation requires a last_synced_at timestamp stored in SharedPreferences. On every sync, you send this timestamp to the API:
- GET /api/changes?since=1718000000000, returns only records modified after that timestamp
- The API returns a diff: new records, updated records, and deleted record IDs
This reduced our TrailMaps sync payload by 94% after the initial download.
Mapbox Offline SDK + Compressed Tile Caching
For the navigation app specifically, we used Mapbox's offline SDK to pre-download map tile packs. Each region pack is ~300MB uncompressed. We reduced this to ~120MB using:
- Vector tiles instead of raster (60% smaller by default)
- Delta tile updates, only download tiles that changed since last update
- SQLite tile cache with LZ4 compression on the tile blob column
This allowed 50+ global regions to be stored within typical device storage constraints.
Conflict Resolution Strategy
When the same record is modified both offline and on the server, you have a conflict. There's no universal right answer, you need a strategy that fits your data.
Last-write-wins is simplest: whichever modification has the later timestamp wins. This works for most user preference data.
Field-level merging is better for complex objects: if the user changed name offline and the server changed status, both changes can coexist. You merge at the field level using the individual field timestamps.
Manual resolution is sometimes necessary for business-critical data. Surface the conflict to the user and let them choose.
Testing Offline Behaviour
The most common mistake is testing only the happy path. Always test:
- App opened with no prior data and no connectivity (empty state, not crash)
- Connectivity lost mid-sync (partial sync recovery)
- Device storage full (graceful degradation)
- Clock skew between device and server (handle negative deltas in timestamps)
Use Flutter's connectivity_plus package to simulate connectivity states in integration tests.
The Result
The TrailMaps app we built achieved 100% offline functionality, a 60% reduction in map bundle size, and a 4.8-star App Store rating. The offline-first architecture is now its primary competitive differentiator, no competing app works reliably without signal.
Need help building something similar?
CodeQuore builds custom software, AI solutions, and scalable applications for startups and enterprises globally.
Get a Free Consultation