Skip to content

Server-Driven UI with XWidget

XWidget lets Flutter apps render UI from external XML resources while keeping business logic in typed Dart code. Instead of compiling every screen directly into the app, you define screens as XWidget fragments, load those fragments from local assets or a resource backend, and let the XWidget runtime inflate them into native Flutter widgets.

This gives you a server-driven UI model without turning your app into an untyped scripting environment. Layout, copy, styling, routes, and composition can move through resources. Controllers, services, custom widgets, permissions, network calls, and platform integrations stay in the compiled Flutter app.

The Model

An XWidget app is built from five pieces:

Piece Role
Resources Deliver fragments, static values, and routes from local assets, XWidget Cloud, or a custom backend.
Fragments XML UI documents that describe widget trees.
Controllers Typed Dart classes that own state, side effects, and business logic.
Dependencies The bridge between Dart values/functions and XML expressions.
Routes URL-style paths or names that resolve to fragments or callback-driven views.

The result is a Flutter app whose UI can be updated through resources while the runtime remains constrained by the widgets, controllers, icons, functions, and types registered at build time.

What Resources Can Contain

A server-driven XWidget resource can include UI fragments:

<Column xmlns="https://xwidget.dev/fragments">
    <Text data="@string/usage_title" />
    <XButton label="@string/refresh" onPressed="${refreshUsage()}" />
</Column>

It can include static value resources such as strings, colors, integers, doubles, and booleans. In production apps, keep static resources split by type. This makes resources easier to review, localize, cache, and update independently.

Typical resource layout:

resources/
  values/
    strings.xml
    colors.xml
    ints.xml
    doubles.xml
    bools.xml
    routes.xml
  fragments/
    app.xml
    pages/
      overview.xml
      usage.xml

Each value file uses a <resources xmlns="https://xwidget.dev/values"> root:

<!-- resources/values/strings.xml -->
<resources xmlns="https://xwidget.dev/values">
    <string name="usage_title">Project usage</string>
    <string name="refresh">Refresh</string>
</resources>
<!-- resources/values/colors.xml -->
<resources xmlns="https://xwidget.dev/values">
    <color name="accent">0xFF60A5FA</color>
    <color name="surface">0xFF0A0A0A</color>
</resources>
<!-- resources/values/ints.xml -->
<resources xmlns="https://xwidget.dev/values">
    <int name="page_size">25</int>
</resources>
<!-- resources/values/doubles.xml -->
<resources xmlns="https://xwidget.dev/values">
    <double name="chart_height">280.0</double>
</resources>
<!-- resources/values/bools.xml -->
<resources xmlns="https://xwidget.dev/values">
    <bool name="show_advanced_filters">true</bool>
</resources>

XWidget loads all value files from the configured values directory, so splitting resources by type does not change how fragments reference them.

Fragments can reference static values with resource directives:

<Text data="@string/usage_title" />
<Container color="@color/accent" />

They can also read resource values from expressions when a widget attribute needs a computed value:

<Text data="${resString('usage_title')}" />

A server-driven resource can also define routes:

<routes>
    <routeGroup name="main" path="/">
        <route path="/overview" fragment="pages/overview" name="overview" />
        <route path="/usage" fragment="pages/usage" name="usage" />
    </routeGroup>
</routes>

Fragments, static resources, and routes can change independently from the app binary, as long as they only reference capabilities that the app has registered.

What Stays in Dart

Business logic should stay in Dart.

Use controllers for anything that needs type safety, app services, credentials, authorization, mutation, analytics, or native platform behavior:

class UsageController extends Controller {
  List<Usage> usage = [];

  @override
  Future<void> init() async {
    usage = await api.getUsage();
  }

  @override
  void bindDependencies() {
    dependencies.setValue('usage', usage);
    dependencies.setValue('refreshUsage', refreshUsage);
  }

  Future<void> refreshUsage() async {
    usage = await api.getUsage();
    setState(() {});
  }
}

The XML fragment decides how data is presented. The controller decides where the data comes from, how it is validated, and which actions are allowed.

Runtime Flow

At startup, XWidget initializes the generated registry, activates resources, and loads fragments, static values, and routes. When a fragment is requested, XWidget parses its XML, evaluates expressions against the current dependency scope, and inflates registered Flutter widgets.

For routed apps, XRouter resolves URL-style paths or route names to fragments. Navigator routes push a fragment onto Flutter's Navigator. Callback routes drive already-rendered multi-page widgets such as PageView, TabBarView, or IndexedStack.

On web, callback route groups can synchronize with the browser URL. XRouter preserves query strings, handles back and forward navigation, and applies the initial browser URL after the first Flutter frame so deep links open the correct server-driven screen.

Production Boundaries

Treat server-driven UI as a controlled runtime surface:

Keep in Resources Keep Compiled
Layout and composition API clients and credentials
Copy, labels, and static values Authentication and authorization
Colors and style resources Payment, storage, and native integrations
Fragment selection Business rules and validation
Routes and page structure Custom widgets and controllers

This boundary keeps remote UI flexible without giving remote resources unlimited control over the app.

Scope

XWidget does not replace Flutter. It gives Flutter apps a resource-driven UI layer with clear boundaries between remote presentation and compiled behavior — screens update through resources while rendering, controllers, and app architecture stay native. For the case for adopting it, see the Introduction.