fassad_ui

Flutter widgets and HTTP client for querying dblm fassad templates — render your database results as charts and tables with a few lines of code.

What is a fassad?

A fassad is a reusable, parameterized query template stored in dblm. You define the query once (dblm fassad create), give it named parameters, and then run it from anywhere — including this Flutter package — without writing SQL or touching a DSN.

Architecture

Flutter app  ──►  fassad_ui  ──►  @dblm/middleware (HTTP)  ──►  dblm  ──►  database

This package talks to @dblm/middleware, which must be running before your app starts.

Setup

1. Start the middleware

npx @dblm/middleware
# set API_KEY=your-secret in .env or environment

2. Add the package

dependencies:
  fassad_ui: ^0.2.0

3. Run flutter pub get

Usage

Query and display data

import 'package:fassad_ui/fassad_ui.dart';

final client = FassadClient(
  baseUrl: 'http://localhost:3000',
  apiKey: 'your-api-key',
);

// Execute a fassad template
final result = await client.run('top-users', params: {'limit': '10'});

// Render as a bar chart (responsive, theme-aware)
FassadChart(
  result: result,
  xColumn: 'name',
  yColumn: 'count',
  type: FassadChartType.bar,
)

// Render as a scrollable table
FassadTable(result: result)

Offline-capable dashboard (stale-while-revalidate)

Use runWithCache() instead of run() to get instant rendering from local cache while fresh data loads in the background. Works offline automatically.

class SalesDashboard extends StatefulWidget {
  const SalesDashboard({super.key});

  @override
  State<SalesDashboard> createState() => _SalesDashboardState();
}

class _SalesDashboardState extends State<SalesDashboard> {
  final _client = FassadClient(
    baseUrl: 'http://localhost:3000',
    apiKey: 'your-api-key',
  );
  final _cache = FassadCache();   // shared instance; persists across app restarts

  late final Stream<FassadCachedResult> _stream;

  @override
  void initState() {
    super.initState();
    _stream = _client.runWithCache(
      'monthly-sales',
      params: {'year': '2024'},
      cache: _cache,
    );
  }

  @override
  void dispose() {
    _client.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<FassadCachedResult>(
      stream: _stream,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        if (!snapshot.hasData) {
          return const CircularProgressIndicator();
        }

        final r = snapshot.data!;
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Show a badge when rendering cached (offline) data
            if (r.isStale)
              Text('Showing cached data from ${r.cachedAt}'),

            FassadChart(
              result: r.data,
              xColumn: 'month',
              yColumn: 'revenue',
              type: FassadChartType.line,
            ),
            FassadTable(result: r.data),
          ],
        );
      },
    );
  }
}

How runWithCache behaves:

Scenario What happens
First launch, online Shows loading indicator briefly → emits fresh result → saves to cache
Subsequent launch, online Emits cached result immediately (no blank screen) → silently updates with fresh data
Offline, cache exists Emits cached result immediately → network error swallowed silently
Offline, no cache Throws FassadException so you can surface the error

Dark / light mode

Charts automatically respect the app theme — no extra configuration needed. Just supply both theme and darkTheme in your MaterialApp:

MaterialApp(
  theme: ThemeData(colorSchemeSeed: Colors.indigo, brightness: Brightness.light),
  darkTheme: ThemeData(colorSchemeSeed: Colors.indigo, brightness: Brightness.dark),
  themeMode: ThemeMode.system,
  home: MyDashboard(),
)

Bar/line colours come from colorScheme.primary, grid lines from colorScheme.outlineVariant, and pie sections cycle through [primary, secondary, tertiary, primaryContainer, secondaryContainer, tertiaryContainer] — all adapting correctly in both modes.

API reference

FassadClient

FassadClient({
  required String baseUrl,  // middleware base URL, e.g. 'http://localhost:3000'
  required String apiKey,   // must match API_KEY set on the middleware
  http.Client? httpClient,  // optional — inject for testing
})
Method Returns Description
list() Future<List<FassadSummary>> List all available fassad templates
show(name) Future<FassadDetail> Get template details and parameter definitions
run(name, {params}) Future<FassadResult> Execute a template and return rows
runWithCache(name, {params, cache}) Stream<FassadCachedResult> Stale-while-revalidate fetch with local persistence
dispose() void Close the underlying HTTP client

Throws FassadException(statusCode, body) on any non-2xx response.

FassadCache

Persists FassadResult data to SharedPreferences keyed by template name + params.

final cache = FassadCache();

await cache.save(name, params, result);       // write
await cache.load(name, params);               // FassadCachedResult?
await cache.remove(name, params);             // delete one entry
await cache.clearAll();                       // wipe all fassad cache entries

Create a single FassadCache instance per app and pass it to every runWithCache call so all templates share the same store.

FassadCachedResult

Returned by runWithCache() stream emissions.

Property Type Description
data FassadResult The query result (columns + rows)
isStale bool true when data came from local cache, false when fresh from network
cachedAt DateTime When the data was last successfully fetched from the network

FassadChart

Property Type Default Description
result FassadResult required Data from client.run() or cached.data
xColumn String required Column name for X axis / pie labels
yColumn String required Column name for Y axis / pie values
type FassadChartType bar Chart type: bar, line, or pie
height double? auto Fixed height in logical pixels; omit to auto-size

The chart is fully responsive — it uses LayoutBuilder to fill available width and scales bar widths, font sizes, and pie geometry accordingly. When height is omitted the chart takes 55 % of available width, clamped between 200 and 420 px.

Shows "No data" when result.rows is empty.

Chart types:

Value Description
FassadChartType.bar Vertical bar chart with adaptive bar widths
FassadChartType.line Curved line chart with shaded area fill
FassadChartType.pie Pie chart with labels, scaled center radius

FassadTable

Property Type Default Description
result FassadResult required Data from client.run() or cached.data
columnWidth double? null Optional fixed column width

Renders a scrollable DataTable (horizontal + vertical scroll). Shows "No rows returned" when empty.

Models

FassadResult

class FassadResult {
  final List<String> columns;
  final List<List<dynamic>> rows;
  final String source;         // database driver, e.g. "postgres"

  // Convenience accessor — returns null if column not found
  dynamic cell(int row, String column);
}

FassadSummary — name, mode, paramCount, createdAt

FassadDetail — name, mode, connection, module, body, params, createdAt, updatedAt

FassadParam — name, kind, required, defaultValue

Requirements

Dependency Version
Dart SDK ≥ 3.0.0
Flutter ≥ 3.10.0
http ^1.2.0
fl_chart ^0.68.0
shared_preferences ^2.2.0

License

MIT

Libraries

fassad_ui