backend_driven_ui 0.5.0
backend_driven_ui: ^0.5.0 copied to clipboard
Server-Driven UI framework for Flutter with ApiWidget - build data-driven interfaces without FutureBuilder boilerplate.
Backend-Driven UI #
Flutter UIs That Update Themselves. Seriously.
Server-driven UI framework with ApiWidget - build data-driven interfaces without FutureBuilder boilerplate.
β¨ Features #
- π― 80+ Built-in Widget Types - Fully interactive apps from JSON
- β‘ Zero App Releases - Update UI from backend JSON instantly
- π 100% Open Source - MIT licensed, yours forever
- π¦ ApiWidget - FutureBuilder's smarter, faster cousin
- π Lightweight & Fast - Optimized parsing, lazy loading
- π Production Ready - Buttons, lists, gestures, caching
- π State Binding -
${state.key}in any prop, reactive rebuilds - π¬ Animations - Entry animations on any widget via
animateprop - π Form Validation -
Formwidget +submitFormaction from JSON - π Cupertino Widgets - iOS-native controls from JSON
- βΏ Accessibility -
Semanticswidget for screen readers - π i18n Validation - Override all error messages globally
πΈ Screenshots #
A WhatsApp-style UI built entirely from backend JSON β zero hardcoded screens.
| Chats | Status | Calls | Settings |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
π Quick Start #
Installation #
dependencies:
backend_driven_ui: ^0.5.0
Global Base URL (optional) #
Set once at app startup to avoid repeating the full URL on every widget:
void main() {
BduiConfig.baseUrl = 'https://api.myapp.com';
runApp(MyApp());
}
All relative endpoints are then resolved automatically:
BackendDrivenScreen(endpoint: '/screens/home') // β https://api.myapp.com/screens/home
ApiWidget(endpoint: '/products') // β https://api.myapp.com/products
Full URLs (https://...) are always used as-is regardless of baseUrl.
π Documentation #
Backend-Driven UI (the wow factor) #
Render an entire screen from a JSON schema your backend returns. No app update needed β change the JSON, the UI changes instantly.
BackendDrivenScreen(
endpoint: '/api/screens/home',
cacheDuration: Duration(minutes: 5),
onNavigate: (route, args) => Navigator.pushNamed(context, route),
)
Backend returns JSON β Flutter renders the UI:
{
"type": "Column",
"props": { "mainAxisAlignment": "center" },
"children": [
{
"type": "Text",
"props": { "text": "Hello from Backend!", "fontSize": 24, "color": "#1976D2" }
},
{ "type": "SizedBox", "props": { "height": 16 } },
{
"type": "ElevatedButton",
"props": { "text": "Shop Now" },
"action": { "type": "navigate", "route": "/shop" }
}
]
}
Colors in JSON
Three formats are supported β use whichever fits your backend:
| Format | Example | Notes |
|---|---|---|
| Named | "color": "blue" |
All Flutter Colors.* names |
Colors.x |
"color": "Colors.deepPurple" |
Flutter notation directly |
Hex #RRGGBB |
"color": "#1976D2" |
Standard CSS hex |
Hex #AARRGGBB |
"color": "#FF1976D2" |
With alpha channel |
| ARGB int | "color": 4278190080 |
Raw Flutter int |
Local Schema (no API call needed)
SchemaWidget.fromJson({
"type": "Column",
"children": [
{ "type": "Text", "props": { "text": "Hello from JSON!", "fontSize": 24 } }
]
})
ApiWidget #
The declarative way to fetch and display API data.
ApiWidget(
endpoint: '/api/products/featured',
method: HttpMethod.get,
// Optional: Headers
headers: {'Authorization': 'Bearer $token'},
// Optional: Request body (for POST/PUT)
body: {'category': 'electronics'},
// Optional: Cache duration
cacheDuration: Duration(minutes: 5),
// Optional: Auto-refresh (polling)
pollInterval: Duration(seconds: 30),
// State widgets
loadingWidget: CircularProgressIndicator(),
successWidget: (data) {
final products = data['products'] as List;
return ProductGrid(products: products);
},
errorWidget: (error) {
return ErrorCard(
message: error,
onRetry: () => setState(() {}),
);
},
// Optional: Empty state
emptyWidget: EmptyState(message: 'No products found'),
// Optional: Callbacks
onSuccess: (data) => print('Loaded ${data.length} products'),
onError: (error) => print('Error: $error'),
)
Using ApiRequest (reusable request config)
Bundle all request parameters into one object β compose it outside the widget tree and reuse across screens:
final productsRequest = ApiRequest(
endpoint: '/api/products',
method: HttpMethod.get,
headers: {'Authorization': 'Bearer $token'},
cacheDuration: Duration(minutes: 5),
);
// Use it directly
ApiWidget(
request: productsRequest,
successWidget: (data) => ProductList(data),
)
// Derive a variant with copyWith
final filteredRequest = productsRequest.copyWith(
endpoint: '/api/products?category=electronics',
);
Injecting a custom HTTP client
Implement BduiHttpClient to swap the network layer β useful for testing or custom HTTP libraries:
class MockHttpClient implements BduiHttpClient {
@override
Future<ApiResponse> get(String url, { ... }) async {
return ApiResponse(statusCode: 200, data: {'products': []});
}
// implement remaining methods...
}
ApiWidget(
endpoint: '/api/products',
httpClient: MockHttpClient(),
successWidget: (data) => ProductList(data),
)
Custom Widget Registration
Extend with your own widgets using SchemaParser.register:
final parser = SchemaParser();
parser.register('ProductCard', (schema, context) {
final props = schema.props ?? {};
return ProductCard(
title: props['title'],
price: props['price'],
imageUrl: props['imageUrl'],
);
});
Use it from backend:
{
"type": "ProductCard",
"props": {
"title": "iPhone 15",
"price": 79999,
"imageUrl": "https://cdn.app.com/iphone15.jpg"
}
}
π― Advanced Features #
State Binding #
Bind backend JSON to reactive state β widgets with ${state.key} refs rebuild automatically when values change.
// Provide a shared state manager (optional β parser creates one automatically)
final stateManager = BduiStateManager();
final parser = SchemaParser(stateManager: stateManager);
Backend JSON:
{
"type": "Column",
"children": [
{
"type": "TextField",
"props": { "hint": "Your name", "stateKey": "name" }
},
{
"type": "Text",
"props": { "text": "Hello, ${state.name}!" }
}
]
}
As the user types, the Text widget updates live β no setState, no streams.
stateKey prop is supported on TextField, TextFormField, Switch, and Checkbox.
setState action β set state from any button press:
{
"type": "ElevatedButton",
"props": { "text": "Activate" },
"action": { "type": "setState", "params": { "key": "status", "value": "active" } }
}
Form Validation #
{
"type": "Form",
"props": { "formKey": "login", "autovalidateMode": "onUserInteraction" },
"children": [
{
"type": "TextFormField",
"props": {
"hint": "Email",
"stateKey": "email",
"validators": ["required", "email"]
}
},
{
"type": "TextFormField",
"props": {
"hint": "Password",
"stateKey": "password",
"obscureText": true,
"validators": ["required", "minLength:8"]
}
},
{
"type": "ElevatedButton",
"props": { "text": "Sign In" },
"action": { "type": "submitForm", "params": { "formKey": "login" } }
}
]
}
autovalidateMode: "disabled" (default) | "always" | "onUserInteraction"
Animations #
Add an entry animation to any widget with the animate prop:
{
"type": "Card",
"props": { "animate": "slideUp" },
"child": { "type": "Text", "props": { "text": "Animated card" } }
}
Full config with timing:
{
"animate": {
"type": "fadeIn",
"duration": 400,
"delay": 150,
"curve": "easeOut"
}
}
Supported types: fadeIn, slideUp, slideDown, slideLeft, slideRight, scale, bounce
Curves: linear, easeIn, easeOut, easeInOut, bounceIn, bounceOut, elasticIn, elasticOut, fastOutSlowIn
PageView #
Swipeable pages from backend JSON:
{
"type": "PageView",
"props": { "scrollDirection": "horizontal" },
"children": [
{ "type": "Text", "props": { "text": "Page 1" } },
{ "type": "Text", "props": { "text": "Page 2" } }
]
}
Dynamic pages with PageView.builder:
{
"type": "PageView.builder",
"props": { "itemCount": 10 },
"child": { "type": "Text", "props": { "text": "Page template" } }
}
Action Handling #
Execute actions from your backend schemas:
{
"type": "navigate",
"route": "/products"
}
Supported actions:
navigate- Navigate to a routepop- Go backreplace- Replace current routepopUntil- Pop to a named route (or root)api- Make API callsshowDialog- Show alert dialogs (supportsonConfirm,onCancel,onDismiss)showSnackBar- Show snackbarsshowBottomSheet- Show modal bottom sheetslaunchUrl- Open a URL (requiresonLaunchUrlcallback)copy- Copy text to clipboardshare- Share text contentsequence- Execute multiple actions in orderconditional- Conditional executioncustom- App-defined custom actions
Caching & Performance #
// Enable widget caching
final parser = SchemaParser(enableCache: true);
// Clear cache when needed
parser.clearCache();
// API caching
ApiWidget(
endpoint: '/api/products',
cacheDuration: Duration(minutes: 5), // Cache for 5 minutes
)
Server-Controlled Caching #
Let your backend control cache behavior per response:
{
"cachePolicy": "cache",
"cacheTTL": 300,
"ui": {
"type": "Column",
"children": [...]
}
}
Cache policies:
cache- Cache response (default)noCache- Never cache, always fetch freshrefresh- Return cached data, refresh in background (stale-while-revalidate)
Handle background refresh:
ApiWidget(
endpoint: '/api/live-data',
onBackgroundRefresh: (newData) {
// UI automatically updates with fresh data
print('Data refreshed in background!');
},
)
Auto-Retry & Error Handling #
ApiWidget(
endpoint: '/api/products',
maxRetries: 3, // Retry up to 3 times
showRetryButton: true, // Show retry button on error
onError: (error) => logError(error),
)
Accessibility (Semantics) #
Wrap any widget with a Semantics node to add screen reader support:
{
"type": "Semantics",
"props": {
"label": "Submit login form",
"button": true,
"enabled": true
},
"child": {
"type": "ElevatedButton",
"props": { "text": "Sign In" },
"action": { "type": "submitForm", "params": { "formKey": "login" } }
}
}
Available props: label, hint, value, button, enabled, readOnly, checked, toggled, selected, header, image, liveRegion, excludeSemantics.
Validation i18n #
Override error message strings globally before runApp:
void main() {
// French
BduiValidatorMessages.required = 'Ce champ est obligatoire';
BduiValidatorMessages.email = 'Adresse e-mail invalide';
BduiValidatorMessages.minLength = (n) => 'Minimum $n caractères';
BduiValidatorMessages.phone = 'NumΓ©ro de tΓ©lΓ©phone invalide';
BduiValidatorMessages.url = 'URL invalide (doit commencer par http/https)';
runApp(MyApp());
}
Restore English defaults at any time: BduiValidatorMessages.reset().
Available message fields: required, email, numeric, phone, url β and factory fields: minLength(n), maxLength(n), min(n), max(n).
GoRouter Integration #
Pass GoRouter's navigation function as onNavigate:
BackendDrivenScreen(
endpoint: '/screens/home',
onNavigate: (route, {arguments}) {
context.go(route, extra: arguments);
},
)
For ApiWidget with schema rendering, pass the same callback to SchemaParser:
final parser = SchemaParser(
onNavigate: (route, {arguments}) => context.go(route, extra: arguments),
);
Riverpod Integration #
Expose BduiStateManager as a provider so Dart code and backend JSON share the same state:
final bduiStateProvider = ChangeNotifierProvider((_) => BduiStateManager());
// In your widget tree:
final stateManager = ref.watch(bduiStateProvider);
final parser = SchemaParser(stateManager: stateManager);
Backend JSON can then write state with setState actions and your Riverpod listeners see the changes immediately β no extra plumbing required.
Pagination with ApiWidget #
Combine ApiWidget with a page counter in BduiStateManager for infinite scroll:
final stateManager = BduiStateManager()..set('page', 1);
final parser = SchemaParser(stateManager: stateManager);
Backend JSON drives the "Load more" button:
{
"type": "ElevatedButton",
"props": { "text": "Load more" },
"action": {
"type": "setState",
"params": { "key": "page", "value": 2 }
}
}
In Dart, watch the page key and re-fetch:
stateManager.addListener(() {
final page = stateManager.get('page') as int? ?? 1;
ref.read(productsProvider(page).notifier).fetch();
});
π Schema Reference #
See the Schema Reference for complete documentation of all 80+ widgets, props, actions, and conditions β also available as SCHEMA_REFERENCE.md.
π± Examples #
Check out the example directory for complete samples:
- ApiWidget Examples - Basic & list API calls with caching
- Backend-Driven UI - JSON schema rendering
- Local Schema - Use JSON without API calls
- Conditional Rendering - Platform & theme-based UI
π€ Contributing #
Contributions are welcome! Feel free to open issues or submit pull requests on GitHub.
π License #
MIT License - see LICENSE file for details.
π Show Your Support #
If you like this package, please give it a β on GitHub!
Built with β€οΈ for the Flutter community




