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 returns

A 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) => Widget

The 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 done

Composed

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.