fassad_ui 0.2.0
fassad_ui: ^0.2.0 copied to clipboard
Flutter widgets and HTTP client for querying dblm fassad templates — render database results as charts and tables.
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