DEV Community

kanta13jp1
kanta13jp1

Posted on

Supabase Realtime Flutter — Complete Guide to Real-Time Sync Patterns

Supabase Realtime × Flutter — Complete Guide to Real-Time Sync Patterns

"Save and see it instantly" is table stakes for modern apps. Supabase Realtime makes it straightforward.

Broadcast: Lightweight Instant Notifications

// Create a channel and send/receive Broadcasts
final channel = supabase.channel('room-1');

channel
  .onBroadcast(
    event: 'cursor',
    callback: (payload) {
      final x = payload['x'] as double;
      final y = payload['y'] as double;
      setState(() => _remoteCursor = Offset(x, y));
    },
  )
  .subscribe();

// Send your own cursor position
Future<void> sendCursor(Offset pos) async {
  await channel.sendBroadcast(
    event: 'cursor',
    payload: {'x': pos.dx, 'y': pos.dy},
  );
}
Enter fullscreen mode Exit fullscreen mode

Use cases: cursor sharing, typing indicators, transient event notifications (nothing that needs DB persistence)

Presence: Syncing Online Status

// Track who's online with Presence
final presenceChannel = supabase.channel('online-users');

presenceChannel
  .onPresenceSync(callback: (payload) {
    // Get everyone's current state
    final state = presenceChannel.presenceState();
    setState(() {
      _onlineUsers = state.values
          .expand((list) => list)
          .map((p) => p.payload['user'] as String)
          .toList();
    });
  })
  .subscribe(
    (status, [_]) async {
      if (status == RealtimeSubscribeStatus.subscribed) {
        // Announce your own presence
        await presenceChannel.track({'user': _userId, 'status': 'active'});
      }
    },
  );

@override
void dispose() {
  presenceChannel.untrack();
  supabase.removeChannel(presenceChannel);
  super.dispose();
}
Enter fullscreen mode Exit fullscreen mode

Postgres Changes: Receive DB Mutations in Real Time

// Subscribe to changes on the tasks table
supabase
  .channel('tasks-changes')
  .onPostgresChanges(
    event: PostgresChangeEvent.all,
    schema: 'public',
    table: 'tasks',
    filter: PostgresChangeFilter(
      type: PostgresChangeFilterType.eq,
      column: 'user_id',
      value: _userId,
    ),
    callback: (payload) {
      switch (payload.eventType) {
        case PostgresChangeEvent.insert:
          final task = Task.fromJson(payload.newRecord);
          setState(() => _tasks.add(task));
        case PostgresChangeEvent.update:
          final updated = Task.fromJson(payload.newRecord);
          setState(() {
            final idx = _tasks.indexWhere((t) => t.id == updated.id);
            if (idx >= 0) _tasks[idx] = updated;
          });
        case PostgresChangeEvent.delete:
          final id = payload.oldRecord['id'] as String;
          setState(() => _tasks.removeWhere((t) => t.id == id));
        default:
          break;
      }
    },
  )
  .subscribe();
Enter fullscreen mode Exit fullscreen mode

Optimistic Update Pattern

// Update the UI first, then sync to DB (improves perceived performance)
Future<void> toggleTask(Task task) async {
  // 1. Optimistic update (instant)
  setState(() {
    final idx = _tasks.indexWhere((t) => t.id == task.id);
    _tasks[idx] = task.copyWith(completed: !task.completed);
  });

  try {
    // 2. Persist to DB
    await supabase
        .from('tasks')
        .update({'completed': !task.completed})
        .eq('id', task.id);
    // Realtime receives the change — UI is already in the correct state
  } catch (e) {
    // 3. Roll back on failure
    setState(() {
      final idx = _tasks.indexWhere((t) => t.id == task.id);
      _tasks[idx] = task;  // restore original
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Broadcast        → Transient events with no DB persistence (cursor, typing)
Presence         → Online status and session management
Postgres Changes → Subscribe to DB mutations (filter to your own data)
Optimistic UI    → Update state first to maximize perceived speed
Enter fullscreen mode Exit fullscreen mode

Realtime channels carry a connection cost — only open the ones you need, and always release them in dispose.

Top comments (0)