Widgets
The Flutter SDK ships behavioral widgets that encode Koolbase’s semantics correctly — auth branching, stale-while-revalidate lists — so you compose them instead of hand-writing the same state machines in every screen.
Why widgets?
Some behaviors are easy to get subtly wrong: showing a login screen for a split second while a returning user’s session restores, treating a cached query result as final when a network refresh is about to replace it, blanking a list because one refresh failed. The widgets own those behaviors so they’re correct by construction — appearance stays entirely yours.
Availability. Widgets ship in koolbase_flutter 10.3.0 and later. React Native components are planned for a future @koolbase/react-native release; this page currently documents the Flutter surface.
KoolbaseAuthGate
Branches the widget tree on authentication state. The gate calls restoreSession() once at mount behind a restoring slot, so a returning user never sees a login screen flash while their persisted session resolves. It seeds synchronously from the current user, follows authStateChanges, and treats an offline restore as signed in — the SDK restores sessions optimistically when the network is unreachable.
KoolbaseAuthGate(
signedIn: (context, user) => HomeScreen(user: user),
signedOut: (context) => const LoginScreen(),
// restoring: optional — defaults to a centered spinner. It deliberately
// does NOT default to the signedOut builder: flashing a login screen at
// a returning user is the exact bug the gate exists to prevent.
)Descendants read auth state without touching statics, and rebuild when it changes:
final scope = KoolbaseAuthScope.of(context);
scope.user; // KoolbaseUser? — null when signed out
scope.status; // restoring / signedIn / signedOut
scope.restoredOffline; // true after an optimistic offline restore — show a
// banner if you want; API calls may fail until
// connectivity returnsA session that fails to restore (none persisted, or the server rejected the refresh token) lands on signedOut — to the user, an unrestorable session is simply signed out, never an error screen.
KoolbaseCollectionList
An opinionated, scrollable, pull-to-refresh list over a collection. Koolbase queries are stale-while-revalidate: a cached result returns immediately, then a network refresh replaces it. The list handles both arrivals — and keeps stale records over a failed refresh, because stale beats blank. Only a first load with nothing to show is an error state, and that state carries a retry.
KoolbaseCollectionList(
collection: 'expenses',
query: (q) => q
.where('user_id', isEqualTo: user.id)
.orderBy('created_at', descending: true),
itemBuilder: (context, record) => ExpenseTile(record),
)Per-item appearance is entirely yours via itemBuilder. The state slots are optional, with these exact signatures:
empty: (BuildContext context) => Widget
loading: (BuildContext context) => Widget
error: (BuildContext context, Object error,
Future<void> Function() retry) => WidgetThe query callback runs for every fetch with a fresh query, and must be deterministic. Query builders mutate their instance, and refresh streams are keyed by query identity (collection + filters + user) — never retain or reuse a query object. The list constructs a fresh one per fetch and hands it to your callback; just configure and return it.
For a collection with a scoped access rule, filter on the rule’s owner field (as reported by koolbase_describe_project or the dashboard). The server enforces the rule either way — but the filter is what makes the query return the signed-in user’s records instead of an empty list.
Custom layouts: KoolbaseCollectionController
The list’s data half is a widget-free ChangeNotifier that owns the fetch, stream subscription, and refresh lifecycle. For grids, slivers, or any custom scroll surface, drive it directly and keep the same correctness:
final controller = KoolbaseCollectionController(
collection: 'expenses',
queryBuilder: (q) => q.where('user_id', isEqualTo: user.id),
);
await controller.load();
controller.status; // loading / loaded / error
controller.records; // List<KoolbaseRecord>
controller.isFromCache; // true until the network arrival replaces the seed
controller.refreshing; // true while an explicit refresh() runs
await controller.refresh();
// ...listen via addListener, dispose() when doneComposed
The widgets are designed to nest — a complete, correct screen is a few lines:
KoolbaseAuthGate(
signedOut: (context) => const LoginScreen(),
signedIn: (context, user) => Scaffold(
appBar: AppBar(title: const Text('My Expenses')),
body: KoolbaseCollectionList(
collection: 'expenses',
query: (q) => q
.where('user_id', isEqualTo: user.id)
.orderBy('created_at', descending: true),
itemBuilder: (context, record) => ListTile(
title: Text(record.data['title']?.toString() ?? record.id),
subtitle: Text('${record.createdAt}'),
),
),
),
)Session restore, the signed-out wall, both stale-while-revalidate arrivals, pull-to-refresh, empty and error states — all handled, none written.