supabase_realtime_widgets

A small set of Flutter widgets that bind directly to Supabase Realtime channels — list, value, and presence builders that own the subscription lifecycle so you don't have to.

pub package License: MIT

Why

supabase_flutter already exposes Realtime via streams and channels, but most apps end up writing the same boilerplate over and over: fetch the initial page, subscribe to changes, merge INSERT/UPDATE/DELETE events into a local list, and tear it all down on dispose(). This package collapses that boilerplate into three widgets:

  • RealtimeListBuilder<T> — a live list, like StreamBuilder but with the merge logic baked in.
  • RealtimeValueBuilder<T> — a single live row, watched by primary key.
  • PresenceBuilder — a live roster of who else is on a channel.

It's intentionally tiny: no state management opinions, no extra abstractions, no lock-in. You provide the model, the fromJson, and the builder. The widgets do the rest.

Install

dependencies:
  supabase_flutter: ^2.5.0
  supabase_realtime_widgets: ^0.1.0

RealtimeListBuilder

import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supabase_realtime_widgets/supabase_realtime_widgets.dart';

class Todo {
  Todo({required this.id, required this.title});
  factory Todo.fromJson(Map<String, dynamic> json) =>
      Todo(id: json['id'] as int, title: json['title'] as String);
  final int id;
  final String title;
}

RealtimeListBuilder<Todo>(
  client: Supabase.instance.client,
  table: 'todos',
  fromJson: Todo.fromJson,
  getId: (t) => t.id,
  orderBy: 'created_at',
  ascending: false,
  builder: (context, todos) => ListView(
    children: [for (final t in todos) ListTile(title: Text(t.title))],
  ),
)

What you get:

  • The initial fetch runs once, ordered and limited per your config.
  • Inserts append. Updates replace by primary key. Deletes remove. The original list is never mutated.
  • dispose() removes the channel automatically.
  • An UPDATE for a row that isn't in the local list (a missed event after a reconnect) appends instead of dropping silently — so the UI converges back to the upstream state.

RealtimeValueBuilder

For a single row that needs to stay live (a header, a detail screen, a profile card):

RealtimeValueBuilder<Profile>(
  client: Supabase.instance.client,
  table: 'profiles',
  id: userId,
  fromJson: Profile.fromJson,
  builder: (context, profile) {
    if (profile == null) return const Text('Profile deleted.');
    return Text('Hi, ${profile.displayName}');
  },
)

If the row is deleted upstream, the builder receives null. No exceptions, no manual existence checks.

PresenceBuilder

For "who's here right now" — collaborative cursors, viewer counts, room rosters:

PresenceBuilder(
  client: Supabase.instance.client,
  channelName: 'room:42',
  self: {'user_id': currentUserId, 'name': currentUserName},
  builder: (context, members) {
    return Wrap(
      spacing: 8,
      children: [
        for (final m in members)
          Chip(label: Text(m['name']?.toString() ?? '?')),
      ],
    );
  },
)

The current user is track'd on subscribe and untrack'd on dispose. Joins, leaves, and the initial sync all rebuild the widget.

Testable list-merge logic

The hard part of any realtime list — applying a Postgres change payload to an in-memory list — lives in a pure-Dart ChangeMerger<T> class that you can unit test without spinning up a Supabase client:

final merger = ChangeMerger<Todo>(
  fromJson: Todo.fromJson,
  getId: (t) => t.id,
);

final next = merger.apply(currentList, payload);

See test/change_merger_test.dart for the full set of cases.

Example app

A complete live-todo example lives in example/. It needs a Supabase project and a 4-column todos table — the README in that directory walks you through it in three steps.

Design notes

  • No state management opinions. These widgets are StatefulWidgets that own their own subscription. Drop them inside Riverpod, BLoC, GetX, or nothing at all.
  • Generic over T. You write the model and the converter; the package never inspects your row shape.
  • Channel names are deterministic. Each widget uses a stable channel name (schema:table:rt-list, etc.) so multiple widgets watching the same table share infrastructure where possible.
  • No reconnect handling beyond what supabase_flutter provides. Supabase Realtime handles socket-level reconnects; this package only handles event merging.

Status

0.1.0 — initial release. APIs are intentionally small and may evolve. Open an issue if you hit something the three widgets can't express.

License

MIT. See LICENSE.

Libraries

supabase_realtime_widgets
A small set of Flutter widgets that bind directly to Supabase Realtime channels — list, value, and presence builders with automatic subscription lifecycle.