Kite docs

Data layer

One interface, three adapters. Screens never talk to HTTP.

The contract

typedef JsonMap = Map<String, Object?>;

abstract interface class DataProvider {
  Future<ListResult> getList(String resource, ListParams params);
  Future<JsonMap>   getOne (String resource, String id);
  Future<JsonMap>   create (String resource, JsonMap data);
  Future<JsonMap>   update (String resource, String id, JsonMap data);
  Future<void>      delete (String resource, String id);
}

Deliberately untyped at the boundary. The template cannot know your models, and forcing a codegen step on someone evaluating a template is how you lose them in the first ten minutes. Decode into your own types above this line.

ListParams and ListResult

const ListParams({
  int page = 1,
  int perPage = 25,
  SortSpec? sort,
  String search = '',
  Map<String, Object?> filters = const {},
});

class ListResult {
  final List<JsonMap> rows;   // this page
  final int total;            // across ALL pages
}

total is the count across every page, not the length of the page you were handed. Getting this wrong still renders a table — it just paginates wrongly, which is the kind of bug nobody notices until a customer reports missing records.

Switching backend

One line, in lib/core/data/mock_data_provider.dart. Every screen follows, including the ⌘K command palette.

// The default — no backend needed
final dataProvider = Provider<DataProvider>((ref) => MockDataProvider());

// A conventional REST API
final dataProvider = Provider<DataProvider>(
  (ref) => RestDataProvider(
    baseUrl: 'https://api.example.com',
    headers: {'Authorization': 'Bearer $token'},
  ),
);

// Supabase
final dataProvider = Provider<DataProvider>(
  (ref) => SupabaseDataProvider(
    url: 'https://xyzcompany.supabase.co',
    anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'),
  ),
);

MockDataProvider

The most important adapter, because it is what someone sees in the first sixty seconds. It does real work — search, filter, sort and paginate all execute against in-memory tables and return a page plus a total — so your screens exercise the same code paths a real backend will.

Seeded deterministically: 1,284 orders, 642 customers, 312 products. Latency is simulated at 180ms and configurable, so loading states are visible during development instead of flashing past.

MockDataProvider(latency: Duration.zero)   // for tests

RestDataProvider

Assumes the shape most JSON APIs already use:

GET    /orders?_page=1&_limit=25&_sort=total&_order=desc&q=ada&status=Paid
GET    /orders/10004
POST   /orders
PATCH  /orders/10004
DELETE /orders/10004

The row count comes from an X-Total-Count header, falling back to a total field on an envelope. Bodies may be a bare array or { "data": [...], "total": n } — both are unwrapped.

If your API spells any of that differently, rest_data_provider.dart is the one file to change. It is about 120 lines.

SupabaseDataProvider

Talks to PostgREST directly over http — deliberately not supabase_flutter. Pulling a large SDK plus its auth, realtime and storage into a template most people will re-point at their own backend is a poor trade. If you want the SDK, this class is the seam to replace.

// Count comes from Content-Range, requested with Prefer: count=exact
Range: 25-49
Content-Range: 25-49/1284

Search needs columns named explicitly — PostgREST has no "search everything". Edit SupabaseDataProvider.searchColumns for your tables.

Errors

Every adapter throws DataProviderException with a readable message. Screens render it through ErrorState, which offers a retry — a spinner that never resolves is worse than an error.

async.when(
  loading: () => const LoadingState(),
  error: (e, _) => ErrorState(
    message: e is DataProviderException ? e.message : '$e',
    onRetry: () => ref.invalidate(listProvider(resource)),
  ),
  data: (result) => …,
)

Writing your own

Implement the five methods. The tests in test/rest_provider_test.dart show the shape — a fake http.Client, then assertions on query construction, total parsing, and a 404 becoming an exception rather than a crash. Copy that file and point it at yours.