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:
They can also read resource values from expressions when a widget attribute needs a computed value:
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.