Rune
Turn Dart widget-construction source strings into live Flutter widgets at runtime.
Rune parses a string of Dart widget syntax (e.g. Column(children: [Text('Hello')])), walks the resulting AST via the official analyzer package, and constructs real Flutter widgets through pre-registered builders.
No dart:mirrors. No eval. No runtime code execution. The widgets that come out are ordinary Flutter widgets — they compose, animate, and perform like hand-written code.
Why
Deliver UI from a server, a CMS, or a designer tool without shipping a new app binary. The source you pass to a RuneView can be edited, A/B-tested, or user-authored. Because Rune only interprets a constrained subset of Dart expression syntax — never executing arbitrary code — it's compatible with Apple App Store and Google Play store-review policies.
Features
- Runtime interpretation, not compilation.
analyzerproduces the AST; Rune walks it. - Store-compliant. No
dart:mirrors, no eval, no on-device code generation. - Layered and open/closed. Adding a new widget is one builder file, one registration, one test — no core change.
- Strict typing. Dart 3 sealed exceptions, pattern matching,
final class,@immutable.dynamicis banned outside the parser boundary. - Single runtime dependency besides Flutter:
analyzer. All other integrations (responsive scaling, state management, routing, ...) live in separate bridge packages. - Rich data binding. Free identifiers (
userName), deep dot-path (user.profile.name), list/map indexing (items[0].title), and data-driven widget lists (for (final item in items) ...) — all resolved against aMap<String, Object?>you supply. - String interpolation.
'Hello, $name!'and'Count: ${n}'substitute data-context values into literal strings. - Named events.
ElevatedButton(onPressed: "submit")routes taps throughRuneView.onEvent(name, args)to the host app. - Extensible. A
RuneBridgepackage registers widget/value/constant/extension handlers with oneregisterInto(config)call.10.w,size.half, and similar receiver-style property access go throughPropertyResolver→ExtensionRegistry. - Typed error surface. Every failure raises a
RuneExceptionsubtype carrying the offending source substring plus a human-readable message.RuneView.fallback+onErrormake failures non-fatal. very_good_analysis-strict. The whole package passes the strict lint floor with zero ignores beyond two documented exceptions.
Install
dependencies:
rune: ^0.1.0
The package is pre-publication; use a git: or path: dependency until a tagged pub.dev release lands. dart pub publish --dry-run currently reports 0 errors / 0 warnings.
Quickstart
import 'package:flutter/material.dart';
import 'package:rune/rune.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Rune Demo')),
body: RuneView(
source: r"""
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hello, $userName!'),
SizedBox(height: 8),
Text('You have ${cart.itemCount} items.'),
SizedBox(height: 16),
for (final item in cart.items)
Card(
child: ListTile(
title: Text(item.title),
subtitle: Text(item.subtitle),
),
),
ElevatedButton(
onPressed: 'checkout',
child: Text('Checkout'),
),
],
)
""",
config: RuneConfig.defaults(),
data: const {
'userName': 'Ali',
'cart': {
'itemCount': 3,
'items': [
{'title': 'Mouse', 'subtitle': '\$19'},
{'title': 'Keyboard', 'subtitle': '\$79'},
{'title': 'Monitor', 'subtitle': '\$299'},
],
},
},
onEvent: (name, [args]) => debugPrint('event: $name'),
fallback: const Text('Failed to render.'),
onError: (error, stack) => debugPrint('Rune error: $error'),
),
),
),
);
}
Note:
ListTileis Flutter's convenience tile — it is not yet in the default builder set and would need registration. The snippet above is illustrative of the syntax shape; swap toPadding(padding: EdgeInsets.all(8), child: Text(...))if you want to run it verbatim against the stock defaults.
A runnable version lives in example/.
Supported source syntax
Current release: v0.1.0 (first minor — Phase 1 through Phase 4 shipped).
| Category | Elements |
|---|---|
| Widgets | Text, SizedBox, Container, Column, Row, Padding, Center, Stack, Expanded, Flexible, Card, Icon, ListView, AppBar, Scaffold, ElevatedButton, TextButton, IconButton |
| Value ctors | EdgeInsets.all/symmetric/only/fromLTRB/zero, Color(hex), TextStyle(...), BorderRadius.circular(n), BoxDecoration(...), Image.network(url), Image.asset(path) |
| Constants | Colors.*, MainAxisAlignment.*, CrossAxisAlignment.*, MainAxisSize.*, TextAlign.*, TextOverflow.*, Alignment.*, BoxFit.*, StackFit.*, Axis.*, FontWeight.*, BoxShape.*, FlexFit.*, ~60 common Icons.* |
| Literals | int, double, bool, null, string, list [...], set/map {...}, adjacent string concat |
| Interpolation | 'Hello $name', 'Count: ${n}' — expressions resolve against data + constants |
| Identifiers | Bare name → data['name']; Type.member → data Map traversal OR constants registry |
| Deep data paths | user.profile.name, items[0].title — any depth of nested maps + list/map indexing |
| Collections | [for (final item in items) Text(item.title)] — data-driven widget lists, nested for-elements, static + for elements interleaved |
| Events | ElevatedButton(onPressed: 'submit', ...) → RuneView.onEvent('submit', []) |
| Property extensions | 10.w, size.half — via RuneBridge packages registering handlers |
Anything outside this surface raises a RuneException (parse, resolve, or unregistered-builder variant). The plans in docs/superpowers/plans/ enumerate the phases that built this set.
Architecture
A unidirectional pipeline:
RuneView (StatefulWidget)
│
▼
RuneConfig
├─ WidgetRegistry — Phase 1-2d widget builders
├─ ValueRegistry — Phase 1-2c value ctors
├─ ConstantRegistry — Colors, enums, Icons
└─ ExtensionRegistry — Phase 3a .w/.px/.half handlers
(+ withBridges([...]) — RuneBridge-packaged third-party contributions)
│
▼
RuneContext (carries data, events, all four registries, optional Flutter BuildContext)
│
▼
DartParser ─────────▶ AstCache (LRU)
│
▼
ExpressionResolver (dispatcher on Expression AST subtype)
├─ LiteralResolver — literals + adjacent-string concat
├─ IdentifierResolver — SimpleIdentifier / PrefixedIdentifier (data-first, constants fallback)
├─ PropertyResolver — PropertyAccess (Map-first for deep paths, extensions for scalars)
├─ InvocationResolver — MethodInvocation / InstanceCreationExpression
└─ (inline) — ListLiteral + ForElement, SetOrMapLiteral, IndexExpression, StringInterpolation
│
▼
Registered widget/value builder
│
▼
Real Flutter Widget
Architecture invariants (imports flow only downward) are enforced by test/architecture/import_flow_test.dart.
Extending
Register a new widget
final class FooBarBuilder implements RuneWidgetBuilder {
const FooBarBuilder();
@override
String get typeName => 'FooBar';
@override
Widget build(ResolvedArguments args, RuneContext ctx) {
return FooBar(
label: args.requirePositional<String>(0, source: 'FooBar'),
isActive: args.getOr<bool>('isActive', false),
);
}
}
final config = RuneConfig.defaults()
..widgets.registerBuilder(const FooBarBuilder());
Register a new constant group
config.constants
..register('BrandTheme', 'primary', const Color(0xFF0088FF))
..register('BrandTheme', 'accent', const Color(0xFFFF6B35));
Source strings can then use Container(color: BrandTheme.primary, ...).
Register a property extension
config.extensions.register('pct', (target, ctx) {
if (target is num) return target / 100;
throw ArgumentError('Expected num for .pct');
});
Source strings can then use SizedBox(width: (50).pct * MediaQuery.of(...)) — or, more realistically, a bridge that uses ctx.flutterContext to do proper responsive math.
Ship a reusable bundle as a bridge
final class BrandBridge implements RuneBridge {
const BrandBridge();
@override
void registerInto(RuneConfig config) {
config.widgets.registerBuilder(const BrandButtonBuilder());
config.constants
..register('BrandTheme', 'primary', const Color(0xFF0088FF))
..register('BrandTheme', 'accent', const Color(0xFFFF6B35));
config.extensions.register('spacing', (t, c) {
if (t is num) return t * 8.0; // 8-pt grid
throw ArgumentError('spacing expects num');
});
}
}
final config = RuneConfig.defaults()
.withBridges(const [BrandBridge(), OtherBridge()]);
The RuneDefaults helper exposes the same surface internally: RuneDefaults.registerWidgets(registry) / registerValues / registerConstants / registerAll(config). Handy for custom configs that want only a subset of defaults.
A live working bridge ships at packages/rune_responsive_sizer — a ~70-line implementation that adds .w / .h / .sp / .dm responsive-sizing extensions. Use it as both a consumer (pair it with rune via path dep) and a reference for writing your own bridges.
Error handling
RuneExceptionis asealed classwith five variants:ParseException—analyzercould not produce an AST.ResolveException— a resolver encountered an unsupported shape or missing extension.UnregisteredBuilderException— a type name has no matching builder (exposestypeName).ArgumentException— a required builder argument was missing or of the wrong type.BindingException— an identifier referenced a key that is not present inRuneDataContext.
- Every exception carries the offending
sourcesubstring plus a human-readablemessage. RuneViewcatches all exceptions, calls the optionalonErrorcallback, then rendersfallback. In debug builds with nofallback, Flutter's red-screenErrorWidgetis shown; in release builds the view silently collapses to an emptySizedBox.RuneEventDispatcher.dispatchis crash-safe: handler throws (including arity mismatches) are caught anddebugPrint-logged; they never escape into the render pipeline.
Testing
flutter test
flutter analyze
Three hundred and sixty-seven tests cover every resolver, every builder, every registry, the architecture invariants, and end-to-end RuneView renders for each phase. Main is kept green at all times; every commit passes both gates under very_good_analysis.
Roadmap
xPhase 1 — parse → resolve → build pipeline with five MVP widgets andEdgeInsets.all. Taggedv0.0.1-phase1.xPhase 2a — named constants, shallow data binding, string interpolation, compound literals. Taggedv0.0.2-phase2a.xPhase 2b — value builders (EdgeInsets.symmetric/only/fromLTRB,TextStyle,Color(hex),BorderRadius.circular,BoxDecoration). Taggedv0.0.3-phase2b.xPhase 2c — layout/chrome widget builders (Padding,Stack,Card,Image.network/.asset,Icon,ListView,AppBar,Scaffold, and more). Taggedv0.0.4-phase2c.xPhase 2d — button widgets (ElevatedButton,TextButton,IconButton) +RuneView.onEventcatch-all event bridge. Taggedv0.0.5-phase2d.xPhase 2e —RuneDefaultshelper, architecture test, pub-readiness. Taggedv0.0.6-phase2e.xPolish —very_good_analysis ^5.1.0migration,CHANGELOG.md, pub metadata. Taggedv0.0.7-polish.xPhase 3a —PropertyResolver+ExtensionRegistry+RuneBridgecontract + shallow data-prefix traversal. Taggedv0.0.8-phase3a.xPhase 3b — deep dot-path (user.profile.name), index access (items[0]), listfor-elements. Taggedv0.0.9-phase3b.xPhase 3c — siblingrune_responsive_sizerdemo package (.w/.h/.sp/.dmvia MediaQuery). Taggedv0.0.10-phase3c.xPhase 4 — performance benchmarks, dev overlay, hot-reload cache invalidation,0.1.0release. Taggedv0.1.0.pub.devpublish pending user action.
Example
See example/ for a runnable demo that exercises the full current feature set.
License
MIT — see LICENSE.
Libraries
- rune
- Rune — runtime Dart-widget-string to Flutter widget interpreter.