# Dynamic Rendering
Build Flutter UIs from JSON at runtime. WDynamic turns a Map structure into a live widget tree with action handling and form state management.
- [Introduction](#introduction)
- [How It Works](#how-it-works)
- [JSON Schema](#json-schema)
- [Action System](#action-system)
- [Form State Management](#form-state-management)
- [Security & Whitelisting](#security-whitelisting)
- [Custom Widget Builders](#custom-widget-builders)
- [Custom Icons](#custom-icons)
- [Error Handling](#error-handling)
- [Related Documentation](#related-documentation)
## Introduction
Dynamic rendering, also known as Server-Driven UI, allows your backend to control the structure and behavior of your Flutter interface at runtime. Instead of hard-coding widget trees in Dart, you define them in JSON and let WDynamic convert them into live widgets.
This approach unlocks powerful capabilities:
- **CMS-driven layouts** - Non-technical teams can author UI configurations through admin panels.
- **A/B testing** - Experiment with different layouts without rebuilding your app.
- **Remote configuration** - Update UI elements instantly via API responses.
- **White-label apps** - Customize entire interfaces per client from a single codebase.
**Traditional approach:**
```dart
// Hard-coded widget tree
WDiv(
className: 'flex gap-4 p-6',
children: [
WButton(onTap: handleSubmit, child: WText('Submit')),
],
)
```
**Dynamic approach:**
```dart
// JSON-driven widget tree
WDynamic(
json: {
'type': 'WDiv',
'props': {'className': 'flex gap-4 p-6'},
'children': [
{
'type': 'WButton',
'props': {
'onTap': {'action': 'handleSubmit'},
},
'children': [
{'type': 'WText', 'props': {'text': 'Submit'}}
],
},
],
},
actions: {'handleSubmit': (args, state) => print('Form: ${state.getAll()}')},
)
```
## How It Works
WDynamic follows a simple pipeline:
```
JSON Map → WDynamicRenderer → Widget Tree
```
The renderer parses your JSON configuration, validates widget types against a security whitelist, instantiates the corresponding Flutter widgets, and wires up action callbacks. All Wind utilities (className, responsive prefixes, state modifiers) work exactly as they do in statically-defined widgets.
Here's a basic example that renders a styled container with text:
```dart
WDynamic(
json: {
'type': 'WDiv',
'props': {
'className': 'p-6 bg-white dark:bg-slate-800 rounded-xl shadow-sm',
},
'children': [
{
'type': 'WText',
'props': {
'text': 'Hello from JSON!',
'className': 'text-xl font-bold text-gray-800 dark:text-white',
},
},
],
},
)
```
The output is identical to writing the equivalent `WDiv` and `WText` widgets directly in Dart.
## JSON Schema
Every widget definition follows a consistent three-key structure:
```json
{
"type": "WidgetType",
"props": { /* widget properties */ },
"children": [ /* nested widgets */ ]
}
```
### type
The widget class name as a string. Must be present in the default whitelist or registered via custom builders.
```dart
{'type': 'WDiv'}
{'type': 'WButton'}
{'type': 'Column'} // Flutter core widgets also supported
```
### props
A map of properties specific to the widget type. Common properties include:
| Property | Description | Example |
|:---------|:------------|:--------|
| `className` | Wind utility classes | `"flex gap-4 p-6"` |
| `text` | Text content (WText) | `"Submit Form"` |
| `placeholder` | Input placeholder (WInput) | `"Enter email..."` |
| `id` | State tracking identifier | `"username"` |
| `onTap`, `onChange`, etc. | Action bindings | `{"action": "submit", "args": {...}}` |
```dart
{
'type': 'WInput',
'props': {
'id': 'email',
'placeholder': 'user@example.com',
'className': 'border border-gray-300 rounded-lg p-3',
'onChange': {'action': 'validateEmail'},
},
}
```
### children
An array of nested widget definitions. Optional for widgets like `WText` or `WIcon`.
```dart
{
'type': 'WDiv',
'props': {'className': 'flex flex-col gap-2'},
'children': [
{'type': 'WText', 'props': {'text': 'First'}},
{'type': 'WText', 'props': {'text': 'Second'}},
],
}
```
## Action System
Interactive widgets (buttons, inputs, checkboxes) trigger actions via props like `onTap`, `onLongPress`, `onDoubleTap`, `onChange`, and `onChanged`. Each action prop references a named handler registered in the `actions` map.
**Supported action props:**
- `onTap` - Single tap/click (WButton, WAnchor)
- `onLongPress` - Long press gesture (WButton, WAnchor)
- `onDoubleTap` - Double tap/click (WButton, WAnchor)
- `onChange` - Value change (WInput, WCheckbox, WSelect, WDatePicker)
- `onChanged` - Alias for onChange
**JSON syntax:**
```dart
{
'onTap': {
'action': 'actionName',
'args': {'key': 'value'}
}
}
```
**Handler signature:**
Action handlers support two signatures. Wind automatically detects which one you provide:
```dart
// Simple signature (args only)
(Map args) {
print('Action triggered with ${args['key']}');
}
// Stateful signature (args + state)
(Map args, WDynamicState state) {
final username = state.get('username');
print('User $username clicked ${args['button']}');
}
```
**Complete example:**
```dart
WDynamic(
json: {
'type': 'WButton',
'props': {
'className': 'bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded',
'onTap': {'action': 'submitForm', 'args': {'formId': 'login'}},
},
'children': [
{'type': 'WText', 'props': {'text': 'Login'}}
],
},
actions: {
'submitForm': (Map args, WDynamicState state) {
final formId = args['formId'];
final email = state.get('email');
final password = state.get('password');
print('Submitting $formId: email=$email');
},
},
)
```
> [!NOTE]
> If an action name is not found in the `actions` map, WDynamic logs a warning and continues silently. This prevents crashes from misconfigured JSON.
## Form State Management
Widgets with an `id` prop automatically register their values in a shared state store. This eliminates manual state wiring for form fields.
**Auto-tracked widgets:**
- `WInput` - Tracks text value
- `WCheckbox` - Tracks boolean checked state
- `WSelect` - Tracks selected value
- `WDatePicker` - Tracks DateTime value
**Accessing state from actions:**
```dart
WDynamic(
json: {
'type': 'WDiv',
'props': {'className': 'flex flex-col gap-4'},
'children': [
{
'type': 'WInput',
'props': {
'id': 'username', // Auto-tracked
'placeholder': 'Enter name...',
},
},
{
'type': 'WButton',
'props': {
'onTap': {'action': 'greet'},
},
'children': [
{'type': 'WText', 'props': {'text': 'Greet'}}
],
},
],
},
actions: {
'greet': (args, state) {
final name = state.get('username') ?? 'Guest';
print('Hello, $name!');
},
},
)
```
**External state access with WDynamicController:**
The controller API allows you to read/write form values from outside the WDynamic widget:
```dart
final controller = WDynamicController();
// Pre-fill values
controller.setValue('email', 'user@example.com');
controller.setValue('newsletter', true);
// Pass to WDynamic
WDynamic(
json: myFormJson,
controller: controller,
actions: myActions,
)
// Read values externally
final email = controller.getValue('email');
final allValues = controller.getAll(); // {'email': '...', 'newsletter': true}
// Listen for changes
final dispose = controller.addListener('email', (value) {
print('Email changed: $value');
});
// Clean up
controller.dispose();
```
**State API reference:**
| Method | Description |
|:-------|:------------|
| `getValue(id)` | Get a single value by id |
| `setValue(id, value)` | Set a value (triggers listeners) |
| `getAll()` | Get all values as a map |
| `reset()` | Clear all values |
| `addListener(id, callback)` | Listen for changes to a specific id |
## Security & Whitelisting
Because JSON can come from untrusted sources (user input, third-party APIs), WDynamic uses a **whitelist-only** security model. Only explicitly allowed widget types can render.
**Default allowed widgets:**
Wind widgets:
- `WDiv`, `WText`, `WButton`, `WImage`, `WIcon`, `WAnchor`, `WInput`, `WCheckbox`, `WSvg`, `WSelect`, `WPopover`, `WDatePicker`, `WSpacer`
Flutter core widgets:
- `Column`, `Row`, `Center`, `SizedBox`, `Expanded`, `Container`, `Wrap`, `Stack`, `Positioned`, `Padding`, `Align`, `Opacity`, `AspectRatio`, `FittedBox`, `ClipRRect`, `Spacer`
**Restricting widgets:**
Use the `denyWidgets` parameter to remove specific types from the whitelist:
```dart
WDynamic(
json: untrustedJson,
denyWidgets: {'WButton', 'Stack'}, // Disable buttons and stacks
)
```
Any attempt to render a denied widget will trigger the `onUnknownWidget` callback or render an empty container.
**Why whitelist-only?**
Arbitrary widget instantiation could expose dangerous APIs (file system access, network calls, etc.). The whitelist ensures that only safe, layout-focused widgets can be dynamically rendered.
## Custom Widget Builders
Extend WDynamic with your own widget types using the `builders` parameter:
```dart
WDynamic(
json: {
'type': 'ProfileCard',
'props': {
'name': 'Alice',
'role': 'Engineer',
'avatar': 'https://example.com/avatar.png',
},
},
builders: {
'ProfileCard': (Map props, List children) {
return WDiv(
className: 'flex items-center gap-4 p-4 bg-white rounded-lg shadow',
children: [
WImage(props['avatar'], className: 'w-12 h-12 rounded-full'),
WDiv(
className: 'flex flex-col',
children: [
WText(props['name'] ?? '', className: 'font-bold'),
WText(props['role'] ?? '', className: 'text-sm text-gray-600'),
],
),
],
);
},
},
)
```
**Builder signature:**
```dart
typedef WWidgetBuilder = Widget Function(
Map props,
List children,
);
```
Custom builders receive the parsed `props` map and any resolved `children` widgets. You can extract typed values, apply conditional logic, or compose complex layouts.
> [!WARNING]
> Custom builders bypass the security whitelist. Only register builders for widgets you trust. Never pass user-provided builder functions.
## Custom Icons
For `WIcon` widgets in your JSON, you can provide custom icon mappings without building a full custom widget builder. Use the `customIcons` prop to map string names to `IconData`:
```dart
WDynamic(
json: {
'type': 'WIcon',
'props': {'icon': 'myCustomIcon'},
},
customIcons: {
'myCustomIcon': Icons.star,
'myOtherIcon': Icons.favorite,
},
)
```
This is a lightweight alternative to custom builders when you only need to extend the icon library. See [WDynamic — Custom Icons](../widgets/w-dynamic.md#custom-icons) for complete configuration details.
## Error Handling
WDynamic provides two callbacks for handling rendering failures:
**onUnknownWidget** - Called when a widget type is not in the whitelist:
```dart
WDynamic(
json: {'type': 'UnsafeWidget', 'props': {}},
onUnknownWidget: (String type, Map props) {
return WText(
'Widget "$type" not allowed',
className: 'text-red-500 p-2 bg-red-50 rounded',
);
},
)
```
**onError** - Called when a widget build throws an exception:
```dart
WDynamic(
json: myJson,
onError: (String type, Object error) {
return WDiv(
className: 'p-4 bg-red-100 border border-red-300 rounded',
child: WText('Error rendering $type: $error'),
);
},
)
```
Both callbacks are optional. If not provided, WDynamic renders an empty `SizedBox.shrink()` for failures.
**Maximum depth protection:**
To prevent infinite recursion or stack overflows from deeply nested JSON, WDynamic enforces a maximum depth limit (default: 50 levels):
```dart
WDynamic(
json: deeplyNestedJson,
maxDepth: 100, // Increase if needed
)
```
## Related Documentation
- [WDynamic Widget](../widgets/w-dynamic.md) - Full API reference
- [State Management](./state-management.md) - Interactive states and WAnchor
- [Utility-First Fundamentals](./utility-first.md) - Wind className syntax
- [WButton](../widgets/w-button.md) - Action-driven button widget
- [WInput](../widgets/w-input.md) - Form input with state tracking