search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

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

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:

// Hard-coded widget tree
WDiv(
  className: 'flex gap-4 p-6',
  children: [
    WButton(onTap: handleSubmit, child: WText('Submit')),
  ],
)

Dynamic approach:

// 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:

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:

{
  "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.

{'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": {...}}
{
  'type': 'WInput',
  'props': {
    'id': 'email',
    'placeholder': '[email protected]',
    '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.

{
  '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:

{
  'onTap': {
    'action': 'actionName',
    'args': {'key': 'value'}
  }
}

Handler signature:

Action handlers support two signatures. Wind automatically detects which one you provide:

// 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:

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:

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:

final controller = WDynamicController();

// Pre-fill values
controller.setValue('email', '[email protected]');
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:

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:

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:

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:

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 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:

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:

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):

WDynamic(
  json: deeplyNestedJson,
  maxDepth: 100,  // Increase if needed
)