pseudo_ui

A Server-Driven UI (SDUI) rendering engine for Flutter. Define your UI once with JSON — render it natively with Material 3 widgets.

The SDK pairs a JSON Schema data contract with a View JSON component tree to produce fully interactive forms, summaries, and multi-step workflows without shipping new client code.

Features

  • 45+ Material Design 3 components — TextField, Dropdown, DatePicker, TabView, Dialog, Slider, Avatar, Chip, and more
  • Delegate-driven architecture — the SDK never makes HTTP calls; your app provides data via a simple interface
  • Expression engine$form, $instance, $param, $ui, $lov, $lookup, $schema, $item, $context namespaces for dynamic value resolution
  • Conditional engineshowIf / hideIf / enableIf / disableIf with allOf, anyOf, not compound rules and 13 operators
  • Validation engine — JSON Schema validation (pattern, format, minLength, min/max) plus async custom validation via delegate
  • LOV & Lookup — List-of-Values dropdowns with cascade filtering and real-time data enrichment
  • Nested components — reusable sub-components with isolated contexts, input contracts (x-binding), and two-way data flow
  • $ui state — transient UI state (dialog visibility, active tab) that never pollutes form data
  • Overlay surfaces — Dialog, BottomSheet, SideSheet, NavigationDrawer open/close driven by $ui state
  • Multi-language — all labels, errors, and enum values support { "en": "...", "tr": "...", "ar": "..." }

Installation

Add to your pubspec.yaml:

dependencies:
  pseudo_ui: ^0.1.1

Or from pub.dev:

flutter pub add pseudo_ui

Quick Start

Two steps: first a minimal form, then we enrich it with a nested component and a dropdown.

Step 1: Minimal form

Name, surname, birth date, and a Submit button.

Schema:

{
  "type": "object",
  "required": ["name", "surname"],
  "properties": {
    "name": { "type": "string", "minLength": 1, "x-labels": { "en": "First Name", "tr": "Ad" } },
    "surname": { "type": "string", "minLength": 1, "x-labels": { "en": "Surname", "tr": "Soyad" } },
    "birthDate": { "type": "string", "format": "date", "x-labels": { "en": "Date of Birth", "tr": "Doğum Tarihi" } }
  }
}

View:

{
  "view": {
    "type": "Column",
    "gap": "md",
    "children": [
      { "type": "TextField", "bind": "name" },
      { "type": "TextField", "bind": "surname" },
      { "type": "DatePicker", "bind": "birthDate" },
      { "type": "Button", "label": { "en": "Submit", "tr": "Gönder" }, "variant": "filled", "action": "submit" }
    ]
  }
}

Dart code:

import 'package:flutter/material.dart';
import 'package:pseudo_ui/pseudo_ui.dart';

class MyPage extends StatelessWidget {
  final DataSchema schema; // loaded from JSON
  final ViewDefinition view; // loaded from JSON

  const MyPage({super.key, required this.schema, required this.view});

  @override
  Widget build(BuildContext context) {
    return PseudoView(
      schema: schema,
      view: view,
      lang: 'en',
      delegate: MyDelegate(),
    );
  }
}

class MyDelegate extends PseudoViewDelegate {
  @override
  RequestDataFn get requestData => (ref, [params]) async {
    throw UnimplementedError('No LOV in this example');
  };

  @override
  Future<({DataSchema schema, ViewDefinition view})> loadComponent(String ref) async {
    throw UnimplementedError('No nested components');
  }

  @override
  Future<void> onAction(String action, Map<String, dynamic> formData, [String? command]) async {
    if (action == 'submit') debugPrint('Form submitted: $formData');
  }
}

Step 2: Add a dropdown with LOV

Add a city field. The dropdown gets options via requestData.

Add to schema:

"city": {
  "type": "string",
  "x-labels": { "en": "City", "tr": "Şehir" },
  "x-lov": {
    "source": "get-cities",
    "valueField": "$.response.data.code",
    "displayField": "$.response.data.name"
  }
}

Add to view children (before Button):

{ "type": "Dropdown", "bind": "city" }

Implement requestData:

@override
RequestDataFn get requestData => (ref, [params]) async {
  if (ref == 'get-cities') {
    return {
      'response': {
        'data': [
          {'code': '06', 'name': 'Ankara'},
          {'code': '34', 'name': 'Istanbul'},
        ]
      }
    };
  }
  throw Exception('Unknown source: $ref');
};

Initial data (optional)

You can pass initial values when the view first renders:

Parameter Purpose
instanceData Backend/persisted data (e.g. read-only display, lookup filters). Used by $instance expressions and summary views.
params Parent-bound parameters for nested components. Used by $param expressions.
PseudoView(
  schema: schema,
  view: view,
  instanceData: {'status': 'active', 'createdAt': '2024-01-01'},
  params: {'branchCode': '001'},
  lang: 'en',
  delegate: delegate,
)

instanceData is for backend state that drives display and lookups. Form data is managed internally by PseudoView based on schema defaults and user input — both instanceData and params are optional.


Lookups (enrichment)

When a schema property has x-lookup, the SDK fetches enrichment data via requestData. You must activate the lookup by listing it in the view's lookups array — otherwise it won't run.

Schema (defines the lookup):

{
  "branchDetail": {
    "type": "object",
    "x-lookup": {
      "source": "get-branch-details",
      "resultField": "$.response.data",
      "filter": [{ "param": "branchCode", "value": "$param.selectedBranchCode", "required": true }]
    }
  }
}

View (activates it):

{
  "dataSchema": "urn:amorphie:res:schema:shared:branch-info",
  "lookups": ["branchDetail"],
  "view": { "type": "Column", "children": [
    { "type": "Text", "content": "$lookup.branchDetail.address" }
  ]}
}

Then use $lookup.branchDetail.address, $lookup.branchDetail.phone, etc. in Text or other components. The SDK calls requestData(source, filterParams) when the view mounts; the delegate returns the enrichment payload.


Architecture

┌─────────────────────────────────────────────────────┐
│                   Your Application                   │
│                                                      │
│  ┌──────────────┐   implements   ┌────────────────┐ │
│  │  MyPage      │ ──────────────▶│  Delegate      │ │
│  │              │                │  - requestData │ │
│  │  PseudoView( │                │  - loadComponent│ │
│  │   schema,    │                │  - onAction    │ │
│  │   view,      │                │  - onLog       │ │
│  │   lang,      │                └────────────────┘ │
│  │   delegate)  │                        ▲          │
│  └──────┬───────┘                        │          │
├─────────┼────────────────────────────────┼──────────┤
│  SDK    │                                │          │
│         ▼                                │          │
│  ┌─────────────────┐  ┌──────────────┐   │          │
│  │ DynamicRenderer  │  │ Expression   │   │          │
│  │ (recursive)      │  │ Resolver     │   │          │
│  │                  │  ├──────────────┤   │          │
│  │ 45+ MD3 widgets  │  │ Conditional  │   │          │
│  │ built-in Flutter │  │ Engine       │   │          │
│  │                  │  ├──────────────┤   │          │
│  │ Overlay surfaces │  │ Schema       │   │          │
│  │ (Dialog, Sheet)  │  │ Resolver     │◀──┘          │
│  │                  │  │ (validation) │              │
│  └─────────────────┘  └──────────────┘              │
└─────────────────────────────────────────────────────┘

Data Model (MVVM)

Layer File Purpose
ViewModel schema.json Data contract — field types, validation, LOV sources, conditionals, multi-lang labels
View view.json UI component tree — layout, binding, actions, transient UI state
Model Backend Persisted data, served via delegate's requestData

Delegate Interface

abstract class PseudoViewDelegate {
  /// Fetch data from backend (LOV items, lookup enrichment).
  RequestDataFn get requestData;

  /// Load a nested component's schema + view by reference.
  Future<({DataSchema schema, ViewDefinition view})> loadComponent(String ref);

  /// Handle user actions (submit, cancel, back, custom commands).
  Future<void> onAction(String action, Map<String, dynamic> formData, [String? command]);

  /// Optional: custom async validation after built-in checks pass.
  Future<String?> onValidationRequest(String field, dynamic value, Map<String, dynamic> formData);

  /// Optional: capture SDK logs.
  void onLog(String level, String message, [dynamic error, Map<String, dynamic>? context]);

  /// Optional: receive nested component lookup data (for debug panels).
  void onNestedLookupData(String componentRef, Map<String, Map<String, dynamic>> lookupData);
}

Supported Components

Layout

Column · Row · ScrollView · Grid · Expanded · Center · Wrap · Divider

Input

TextField · TextArea · NumberField · Dropdown · Checkbox · RadioGroup · DatePicker · TimePicker · Switch · Slider · SegmentedButton · SearchField · AutoComplete

Display

Text · Icon · Image · Avatar · Chip · Badge · ListTile · RichText · ProgressIndicator · LoadingIndicator

Surface & Overlay

Card · Dialog · BottomSheet · SideSheet · Snackbar · Tooltip

TabView · AppBar · NavigationBar · NavigationDrawer

Container

ExpansionPanel

Action

Button · IconButton · FAB · Menu · Toolbar

Control

ForEach · Component (nested)

Expression Namespaces

Namespace Source Example
$form.field User input data $form.firstName
$instance.field Backend persisted data $instance.status
$param.field Parent-bound data (nested components) $param.cityCode
$ui.key Transient UI state (not submitted) $ui.showDialog
$schema.field.label Schema label for current language $schema.city.label
$lov.field LOV items array $lov.city
$lov.field.display Localized display name for current value $lov.city.display
$lookup.prop.field Enrichment data $lookup.branch.address
$item.field ForEach iteration item $item.name
$context.lang Runtime context $context.lang

Conditional Operators

equals · notEquals · in · notIn · greaterThan · lessThan · greaterThanOrEquals · lessThanOrEquals · contains · startsWith · endsWith · isEmpty · isNotEmpty

Compound rules: allOf (AND), anyOf (OR), not (negate) — recursive nesting supported.

Validation Formats

Built-in format validators: email · uri / url · date · date-time · time · phone / tel · iban

Vocabularies

The repository includes shared JSON Schema vocabulary definitions for IDE auto-complete and tooling (in the repo root vocabularies/ folder):

  • View Vocabulary (view-vocabulary.json) — defines all valid component types, properties, and their constraints
  • ViewModel Vocabulary (view-model-vocabulary.json) — defines all x-* extensions (x-labels, x-lov, x-conditional, etc.)

Both TS and Dart SDKs share the same vocabulary definitions, ensuring consistent component behavior across platforms.

Testing

cd core/dart-pseudo-ui
flutter test

101 tests covering expression resolver, schema resolver, conditional engine, bind resolver, data client, widget rendering, overlay surfaces, and LOV cascading.

Theming

The pseudo-ui Flutter adapter integrates with Material 3 theming. Form fields respect InputDecorationTheme, text uses TextTheme variants, and colors follow ColorScheme roles. For semantic colors (success, warning, info), use the PseudoUiThemeExt theme extension.

See the Theming Guide for comprehensive theming documentation.

Cross-Platform

This is the Dart/Flutter implementation. A companion TypeScript package (@burgan-tech/pseudo-ui on npm) renders the same JSON schemas and views using PrimeVue, PrimeReact or Angular Material components. Both share the same vocabulary definitions and expression engine, ensuring consistent behavior across platforms.

License

MIT

Libraries

pseudo_ui
Pseudo UI — Server-Driven UI rendering engine for Flutter.