Home Services Work About Blog Contact Let's Talk
BlogFlutter Apps
Flutter Apps

Offline-First Flutter App for Field Service: SQLite Sync with SharePoint

Why Offline-First Matters for Field Service

Field service workers — engineers on factory floors, technicians in remote locations, inspectors in facilities with spotty Wi-Fi — cannot afford to wait for connectivity. An offline-first Flutter app treats the local device as the primary source of truth and syncs with SharePoint when the network becomes available, instead of blocking the user on every operation.

This pattern differs fundamentally from "offline fallback" where the app shows a cached read-only screen. In a true offline-first app, technicians can create work orders, capture inspection photos, update job status, and collect digital signatures — all stored locally in SQLite and pushed to SharePoint Lists when connectivity returns.

Architecture Principle

Write all data to SQLite first. Never block a UI operation on a network call. The sync layer runs independently in the background — your UI should never await a network response.

Setting Up sqflite for Local Persistence

The sqflite package is Flutter's standard SQLite wrapper. Combined with path_provider, you get a robust local persistence layer that works identically on Android and iOS.

pubspec.yaml — Core Dependencies
dependencies:
  sqflite: ^2.3.2
  path_provider: ^2.1.2
  path: ^1.9.0
  connectivity_plus: ^5.0.2
  workmanager: ^0.5.2
  msal_auth: ^1.0.0

Always version your database schema. Future releases will need to migrate existing user data — never drop and recreate the database on upgrade.

database_helper.dart — Schema with versioning
class DatabaseHelper {
  static Database? _db;

  static Future<Database> get() async {
    _db ??= await openDatabase(
      join(await getDatabasesPath(), 'fieldservice.db'),
      version: 1,
      onCreate: (db, v) async {
        await db.execute('''
          CREATE TABLE work_orders (
            id TEXT PRIMARY KEY,
            title TEXT NOT NULL,
            status TEXT DEFAULT 'pending',
            technician TEXT,
            site TEXT,
            notes TEXT,
            created_at INTEGER,
            server_etag TEXT,
            synced INTEGER DEFAULT 0,
            dirty INTEGER DEFAULT 1
          )''');
        await db.execute('''
          CREATE TABLE sync_queue (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            table_name TEXT,
            record_id TEXT,
            operation TEXT,
            payload TEXT,
            attempts INTEGER DEFAULT 0,
            created_at INTEGER
          )''');
      },
    );
    return _db!;
  }
}

The Sync Queue Pattern

Every write (create, update, delete) writes to the local SQLite table AND inserts an entry into sync_queue. A background isolate drains this queue when the network is available, sending changes to SharePoint via REST. This completely decouples the UI from network availability.

work_order_repository.dart — Write with queue
Future<void> createWorkOrder(WorkOrder order) async {
  final db = await DatabaseHelper.get();
  await db.insert('work_orders', order.toMap());
  // Enqueue for sync
  await db.insert('sync_queue', {
    'table_name': 'work_orders',
    'record_id': order.id,
    'operation': 'CREATE',
    'payload': jsonEncode(order.toSharePointMap()),
    'created_at': DateTime.now().millisecondsSinceEpoch,
  });
}
sync_service.dart — Queue drain
Future<void> processQueue() async {
  final db = await DatabaseHelper.get();
  final pending = await db.query('sync_queue',
      where: 'attempts < 3', orderBy: 'created_at ASC');

  for (final item in pending) {
    try {
      await _postToSharePoint(item);
      await db.delete('sync_queue',
          where: 'id = ?', whereArgs: [item['id']]);
    } catch (e) {
      await db.update('sync_queue',
          {'attempts': (item['attempts'] as int) + 1},
          where: 'id = ?', whereArgs: [item['id']]);
    }
  }
}

MSAL Token Refresh on Reconnect

After hours offline, the MSAL access token has expired. Before draining the sync queue, attempt a silent token acquisition. If the refresh token is also expired (rare for tokens with 90-day lifetime), fall back to interactive login.

auth_service.dart — Silent token refresh
Future<String> getValidToken() async {
  try {
    final result = await _msalPlugin.acquireTokenSilent(
      scopes: ['https://graph.microsoft.com/.default'],
    );
    return result.accessToken;
  } on MsalException catch (e) {
    if (e.errorCode == 'no_account_found') {
      return _interactiveLogin();
    }
    rethrow;
  }
}

Conflict Resolution Strategy

Two technicians updating the same work order offline is a real scenario. Implement a pragmatic strategy: store the SharePoint ETag locally with each record. When syncing a PATCH, include If-Match: <etag> in the request header. If SharePoint returns 412 Precondition Failed, a conflict exists.

For most field data (status updates, notes), a last-write-wins policy is acceptable. For sign-off records and completed inspections, show a merge screen letting the technician review both versions and choose which fields to keep.

Critical

Never silently overwrite completed job records. A technician's signed-off inspection being overwritten by a stale offline update is a compliance and safety issue. Always prompt the user for completed records.

Background Sync with WorkManager

Use the workmanager Flutter plugin to register a periodic task that drains the sync queue even when the app is backgrounded. On Android this uses WorkManager's PeriodicWorkRequest; on iOS it uses BGTaskScheduler.

main.dart — Register periodic background sync
Workmanager().initialize(callbackDispatcher, isInDebugMode: false);
Workmanager().registerPeriodicTask(
  'syncTask',
  'fieldServiceSync',
  frequency: const Duration(minutes: 15),
  constraints: Constraints(
    networkType: NetworkType.connected,
    requiresBatteryNotLow: true,
  ),
);

void callbackDispatcher() {
  Workmanager().executeTask((task, data) async {
    final sync = SyncService();
    final token = await AuthService().getValidToken();
    await sync.processQueue(token);
    return Future.value(true);
  });
}

UI Offline Indicators

Users must always know whether they are online or offline and how many changes are pending sync. A persistent banner at the top of every screen (not a dismissible snackbar) is the correct pattern. Pair it with a live count from the sync_queue table.

offline_banner.dart — Persistent connectivity indicator
class OfflineBanner extends ConsumerWidget {
  const OfflineBanner({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isOnline = ref.watch(connectivityProvider);
    final pendingCount = ref.watch(pendingSyncCountProvider);
    if (isOnline && pendingCount == 0) return const SizedBox.shrink();
    return Container(
      color: isOnline ? Colors.orange.shade700 : Colors.red.shade700,
      padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
      child: Text(
        isOnline
          ? 'Syncing $pendingCount change(s)...'
          : 'Offline — $pendingCount change(s) pending',
        style: const TextStyle(color: Colors.white, fontSize: 13),
      ),
    );
  }
}

Testing Offline Scenarios

Test offline behaviour systematically: use Android Emulator's network throttle, iOS Simulator's Network Link Conditioner, or connectivity_plus mocks in unit tests. Write integration tests that perform writes offline, assert the sync queue has entries, simulate reconnection, and verify SharePoint receives the expected payloads.

Key Takeaways

Write all data to SQLite first — never block on a network call in the UI layer.

Use a sync_queue table as an outbox; drain it in WorkManager background tasks every 15 minutes.

Silently refresh MSAL access tokens before each sync cycle — tokens expire during extended offline periods.

Use SharePoint ETags for conflict detection; show a merge UI for high-stakes completed records.

Show a persistent top banner for offline state — never rely on dismissible snackbars for connectivity feedback.

AT

Akshara Technologies

Microsoft 365 & Flutter Specialists

We have delivered offline-first Flutter apps for field technicians across manufacturing, facilities management, and utilities — all syncing with SharePoint and Microsoft 365.

Need a Flutter field service app?

Our Flutter team builds offline-first mobile solutions that sync with SharePoint and Microsoft 365. From field inspection apps to work order management — let us help you ship.

Start a Conversation View Flutter Services

Related Articles