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.
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, likeStreamBuilderbut 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
UPDATEfor 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_flutterprovides. 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.