flutter_sdui_kit 0.3.0 copy "flutter_sdui_kit: ^0.3.0" to clipboard
flutter_sdui_kit: ^0.3.0 copied to clipboard

A server-driven UI framework for Flutter. Render native widgets from JSON with 23 components, data binding, forms, theming, and action handling.

flutter_sdui_kit #

pub package License: MIT

A server-driven UI framework for Flutter. Your server sends JSON → the SDK renders native widgets. Ship UI changes instantly — no app-store release.


Quick Start #

1. Install #

# pubspec.yaml
dependencies:
  flutter_sdui_kit: ^0.1.1

2. Render a screen (3 lines) #

import 'package:flutter_sdui_kit/flutter_sdui_kit.dart';

// That's it. Pass the JSON string from your API.
SduiWidget(json: serverResponseJson)

3. Handle actions + pass data #

// Create an action handler.
final actions = ActionHandler();
actions.register('navigate', (action, payload) {
  Navigator.pushNamed(context, payload['route'] as String);
});

// Render with data for templates and visibility conditions.
SduiWidget(
  json: serverJson,
  actionHandler: actions,
  data: {
    'user': {'name': 'Alice', 'is_premium': true},
    'cart': {'count': 3},
  },
)

That's the complete setup. Everything below is reference.


How It Works #

Server JSON  →  SduiWidget  →  Flutter Widgets
  1. Server sends a JSON response with a body node tree
  2. SduiWidget parses it, resolves {{templates}}, evaluates visible_if conditions
  3. Each node's type maps to a builder function that returns a Flutter widget
  4. User interactions fire actions back to your registered handlers

JSON Protocol #

Framework-agnostic. The same JSON works with any client SDK (Flutter, SwiftUI, Compose, React Native) that implements the protocol.

Screen Envelope #

{
  "screen": "home",
  "version": 1,
  "cache_ttl": 300,
  "theme": { "primary": "#6C63FF", "background": "#FFFFFF", "text": "#1A1A2E" },
  "body": { ... }
}

Node Structure #

Every UI element is a node:

{
  "type": "text",
  "props": { "content": "Hello, {{user.name}}!", "visible_if": "user.is_premium" },
  "action": { "type": "navigate", "payload": { "route": "/profile" } },
  "children": []
}
Field Type Description
type string Component type (e.g. text, column, button)
props object Properties for the component
action object? Action fired on user interaction
children node[] Child nodes

Built-in Components (23 types) #

Layout #

Type Key Props
column spacing, cross_alignment, main_alignment, scroll
row spacing, cross_alignment, main_alignment, scroll
padding all, horizontal, vertical, left, right, top, bottom
sizedbox width, height
container background, corner_radius, width, height, padding
scroll direction (horizontal / vertical)
safe_area top, bottom, left, right
expanded flex, fit (tight / loose)
center
aspect_ratio ratio
constrained_box min_width, max_width, min_height, max_height

Content #

Type Key Props
text content, style (heading/subheading/body/caption), color, max_lines, text_align
image url, aspect_ratio, corner_radius, fit
button label, variant (primary/outline/text), full_width, background, text_color, corner_radius
icon name, size, color
card corner_radius, background, elevation, width
list direction, spacing, padding, height, width
divider color, thickness

Form #

Type Key Props Auto-fires
text_input placeholder, value, field, max_lines, obscure input_changed
checkbox checked, label, field, size, active_color input_changed
switch value, label, field, active_color input_changed
dropdown options [{label,value}], selected, placeholder, field input_changed

Interaction #

Type Key Props
gesture behavior (opaque/translucent/defer); requires action on node

Flex children: Any child node can have "flex": <int> in its props to be wrapped in Expanded inside a column/row. Add "flex_fit": "loose" for Flexible instead.


Data Binding #

Use {{path}} in any string prop. Pass a data map to resolve:

SduiWidget(
  json: serverJson,
  data: {'user': {'name': 'John'}, 'cart': {'count': 3}},
)
{ "type": "text", "props": { "content": "Hello {{user.name}}! {{cart.count}} items." } }

Hello John! 3 items.


Conditional Visibility #

Add visible_if to any node's props:

{ "type": "text", "props": { "content": "VIP only", "visible_if": "user.is_premium" } }

Supported: truthy, !negation, ==, !=, >, <, >=, <=, &&, ||

"visible_if": "user.role == admin && cart.count > 0"

Action Handling #

final actions = ActionHandler();

actions.register('navigate', (action, payload) {
  Navigator.pushNamed(context, payload['route'] as String);
});

actions.register('api_call', (action, payload) async {
  await http.post(Uri.parse(payload['endpoint'] as String));
});

actions.onUnhandled = (action, payload) {
  debugPrint('Unknown action: ${action.type}');
};

Form components auto-fire input_changed with {"field": "...", "value": ...}.


Error Handling #

SduiWidget(
  json: serverJson,

  // Called for every error (parse, render, expression).
  // Send to Crashlytics / Sentry / your logger.
  onError: (error) => logger.warning('${error.type}: ${error.message}'),

  // Replaces individual broken nodes (siblings keep rendering).
  errorWidgetBuilder: (error) => Text('Error in ${error.nodeType}'),

  // Shown when JSON is null, empty, or fails to parse entirely.
  fallback: CircularProgressIndicator(),
)

Errors never crash the tree. A broken node becomes SizedBox.shrink() (or your errorWidgetBuilder), and its siblings render normally.


Custom Components #

final registry = createDefaultRegistry();

registry.register('rating_stars', (node, context) {
  final filled = node.props['filled'] as int? ?? 0;
  return Row(
    mainAxisSize: MainAxisSize.min,
    children: List.generate(5, (i) => Text(i < filled ? '★' : '☆')),
  );
});

SduiWidget(json: serverJson, registry: registry)

Server sends: { "type": "rating_stars", "props": { "filled": 4 } }


State Management #

SduiWidget is a plain StatelessWidget. It has zero opinions about state management. It takes json and data as inputs — whenever those change, the UI updates. This works with everything:

setState (simplest) #

class MyScreen extends StatefulWidget {
  @override
  State<MyScreen> createState() => _MyScreenState();
}

class _MyScreenState extends State<MyScreen> {
  String? _json;
  Map<String, dynamic> _data = {};

  @override
  void initState() {
    super.initState();
    _fetchScreen();
  }

  Future<void> _fetchScreen() async {
    final response = await http.get(Uri.parse('https://api.example.com/screen/home'));
    setState(() => _json = response.body);
  }

  @override
  Widget build(BuildContext context) {
    final actions = ActionHandler();
    actions.register('navigate', (a, p) {
      Navigator.pushNamed(context, p['route'] as String);
    });
    actions.register('input_changed', (a, p) {
      setState(() => _data = {..._data, p['field']: p['value']});
    });

    return SduiWidget(
      json: _json,
      actionHandler: actions,
      data: _data,
      fallback: Center(child: Text('Loading...')),
    );
  }
}

Riverpod #

final screenProvider = FutureProvider<String>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/screen/home'));
  return response.body;
});

final formDataProvider = StateProvider<Map<String, dynamic>>((ref) => {});

class MyScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final screenAsync = ref.watch(screenProvider);
    final formData = ref.watch(formDataProvider);

    final actions = ActionHandler();
    actions.register('navigate', (a, p) {
      Navigator.pushNamed(context, p['route'] as String);
    });
    actions.register('input_changed', (a, p) {
      ref.read(formDataProvider.notifier).state = {
        ...ref.read(formDataProvider),
        p['field']: p['value'],
      };
    });

    return screenAsync.when(
      data: (json) => SduiWidget(json: json, actionHandler: actions, data: formData),
      loading: () => Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Text('Error: $e')),
    );
  }
}

Bloc / Cubit #

class ScreenCubit extends Cubit<ScreenState> {
  ScreenCubit() : super(ScreenLoading());

  Future<void> load() async {
    final response = await http.get(Uri.parse('https://api.example.com/screen/home'));
    emit(ScreenLoaded(json: response.body));
  }
}

// In widget:
BlocBuilder<ScreenCubit, ScreenState>(
  builder: (context, state) {
    if (state is ScreenLoaded) {
      return SduiWidget(json: state.json, actionHandler: actions);
    }
    return CircularProgressIndicator();
  },
)

GetX / Provider / MobX / etc. #

Same pattern — feed json and data from your store. SduiWidget rebuilds when its inputs change. No wrappers, no adapters, no lock-in.

Sharing data across nested SduiWidgets #

SduiDataProvider(
  data: {'user': {'name': 'Alice'}},
  child: SduiWidget(json: screenJson),
)

Local data on SduiWidget overrides ancestor SduiDataProvider data for the same keys.


Layout Safety #

The SDK handles common infinite-layout pitfalls automatically:

Pattern How it's handled
Column in Column mainAxisSize: MainAxisSize.min — no unbounded assertion
Column in ScrollView Same — Column sizes to content
List in Column shrinkWrap: true + NeverScrollableScrollPhysics
Horizontal list in Column Server sends height prop, SDK wraps in SizedBox
Expanded outside Flex Renderer error boundary catches and replaces with fallback
Bad builder throws Error boundary catches per-node, siblings keep rendering

Architecture #

Server JSON  →  SduiScreen (model)  →  SduiRenderer  →  Flutter Widgets
                     ↓                       ↓
                SduiNode tree         ComponentRegistry
                                      TemplateResolver
                                      ExpressionEvaluator
                                      ActionHandler
Concept What it does
SduiNode Recursive tree: type, props, children, action
ComponentRegistry Maps type string → builder function
ActionHandler Maps action type → callback
TemplateResolver {{path}} → value from data map
ExpressionEvaluator visible_if → boolean
SduiRenderer Walks tree, resolves templates/conditions, calls builders, catches errors

Implementing for Other Platforms #

The JSON protocol is platform-agnostic. To build an SDK for SwiftUI / Compose / React Native:

  1. Parse the screen envelope into native models
  2. Build a component registry (type → native view builder)
  3. Build an action handler (action type → callback)
  4. Implement template resolution (regex {{path}})
  5. Implement expression evaluation (visible_if)
  6. Walk the node tree: resolve → check visibility → build

Same JSON, any platform.


API Reference #

SduiWidget #

Property Type Default Description
json String? required JSON string from server
registry ComponentRegistry? built-in (23 types) Component builders
actionHandler ActionHandler? no-op Action dispatcher
data Map<String, dynamic> {} Data for templates + conditions
fallback Widget SizedBox.shrink() Shown when JSON is null/empty/invalid
onError SduiErrorCallback? Called for every error
errorWidgetBuilder SduiErrorWidgetBuilder? Replacement widget for broken nodes

ComponentRegistry #

Method Description
register(type, builder) Add a builder
registerAll(map) Add multiple builders
unregister(type) Remove a builder
setFallback(builder) Override fallback for unknown types
has(type) / resolve(type) Check / get builder

ActionHandler #

Method Description
register(type, handler) Add a handler
registerAll(map) Add multiple handlers
handle(action) Dispatch an action
onUnhandled Catch-all for unknown types

SduiError #

Property Type
type SduiErrorType (parse, render, unknownComponent, expression)
message String
nodeType String?
exception Object?
stackTrace StackTrace?

License #

MIT — see LICENSE.

1
likes
160
points
300
downloads

Documentation

API reference

Publisher

verified publisherchinmaysinghmodak.com

Weekly Downloads

A server-driven UI framework for Flutter. Render native widgets from JSON with 23 components, data binding, forms, theming, and action handling.

Homepage
Repository (GitHub)
View/report issues

Topics

#sdui #server-driven-ui #json #ui-framework #dynamic-ui

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_sdui_kit