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.
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.
- 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, with 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.
dependencies:
rune: ^1.21.0The 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.
Upgrading from an earlier release? See MIGRATION.md for version-to-version notes. API reference documentation is generated by dart doc from the source; pub.dev automatically builds and hosts it on each published release.
This README is the 5-minute tour. For deeper material, see guides/:
guides/getting-started.md: derin quickstart, data binding, events, error handling.guides/source-syntax.md: desteklenen Dart syntax'ının tam referansı.guides/cookbook.md: copy-paste recipes.guides/bridges.md: 5 sibling bridge paketinin setup ve kullanımı.guides/devtools.md: Flutter DevTools extension.guides/troubleshooting.md: common errors, exception hierarchy.
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'),
),
),
),
);
}A runnable version lives in example/.
Current release: v1.21.0. Borders, gradients, and time breadth. Border.all, Border.symmetric, and standalone BorderSide close the decoration-border gap so source can write BoxDecoration(border: Border.all(color: Colors.red, width: 2)). Three new gradient value builders (LinearGradient, RadialGradient, SweepGradient) plug straight into BoxDecoration(gradient: ...) with stops lists that auto-coerce num to double. BorderStyle and TileMode enums land as constants. On the runtime side, Duration exposes its six unit projections (.inDays, .inHours, .inMinutes, .inSeconds, .inMilliseconds, .inMicroseconds), and DateTime gains field accessors (.year, .month, .day, .hour, .minute, .second, .weekday, ...) plus five method dispatchers (.isBefore, .isAfter, .difference, .add, .subtract). Previous: v1.20.0 geometry constructors, color breadth, state-management trio (see CHANGELOG for the full bundle).
| Category | Elements |
|---|---|
| Widgets | Text, SizedBox, Container, Column, Row, Padding, Center, Stack, Expanded, Flexible, Card, Icon, ListView, AppBar, Scaffold, ElevatedButton, TextButton, IconButton, TextField, Switch, Checkbox, ListTile, Divider, Spacer, GestureDetector, InkWell, SingleChildScrollView, Wrap, AspectRatio, Positioned, Slider, Radio, CheckboxListTile, SwitchListTile, RadioListTile, AnimatedContainer, AnimatedOpacity, AnimatedPositioned, BottomNavigationBar, TabBar, Tab, DropdownButton, DropdownMenuItem, FloatingActionButton, Chip, ChoiceChip, FilterChip, Badge, CircularProgressIndicator, LinearProgressIndicator, Hero, AnimatedSwitcher, AnimatedCrossFade, AnimatedSize, GridView.count, GridView.extent, Drawer, SafeArea, Visibility, Opacity, ClipRRect, ClipOval, Tooltip, CustomScrollView, SliverList, SliverToBoxAdapter, SliverAppBar, SliverPadding, SliverFillRemaining, SliverGrid.count, SliverGrid.extent, FittedBox, ColoredBox, DecoratedBox, Offstage, Semantics, ConstrainedBox, LimitedBox, UnconstrainedBox, FractionallySizedBox, NavigationBar, NavigationRail, StatefulBuilder, RuneCompose, FutureBuilder, StreamBuilder, LayoutBuilder, OrientationBuilder, AlertDialog, SimpleDialog, SimpleDialogOption, Dialog, PopupMenuButton, PopupMenuItem, PopupMenuDivider, FilledButton, OutlinedButton, SegmentedButton, SearchBar, SearchAnchor, Form, TextFormField, Focus, FocusScope, Draggable, LongPressDraggable, DragTarget, Dismissible, InteractiveViewer, ReorderableListView, DataTable, ExpansionTile, ExpansionPanelList, Stepper, FadeTransition, SlideTransition, ScaleTransition, RotationTransition, SizeTransition, AnimatedBuilder, ListenableBuilder, CheckedPopupMenuItem, BottomSheet, PaginatedDataTable |
| Value ctors | EdgeInsets.all/symmetric/only/fromLTRB/zero, Color(hex), Color.fromARGB(a, r, g, b), Color.fromRGBO(r, g, b, o), TextStyle(...), BorderRadius.circular(n), BorderRadius.all(radius), BorderRadius.only(topLeft, topRight, bottomLeft, bottomRight), BorderRadius.vertical(top, bottom), BorderRadius.horizontal(left, right), Radius.circular(x), Radius.elliptical(x, y), Border.all(color, width, style, strokeAlign), Border.symmetric(vertical, horizontal), BorderSide(color, width, style, strokeAlign), LinearGradient(colors, begin, end, stops, tileMode), RadialGradient(colors, center, radius, stops, tileMode), SweepGradient(colors, center, startAngle, endAngle, stops, tileMode), BoxDecoration(color, borderRadius, shape, border, gradient), Image.network(url), Image.asset(path), Duration(...), BottomNavigationBarItem(...), Transform.scale/.rotate, Transform.translate, Transform.flip, Offset(dx, dy), Positioned.fill(child), BoxConstraints(...), NavigationDestination(...), NavigationRailDestination(...), RuneComponent(...), TextEditingController(...), ScrollController(...), FocusNode(...), PageController(...), ListView.builder, GridView.countBuilder, GridView.extentBuilder, SliverList.builder, SliverGrid.countBuilder, SliverGrid.extentBuilder, SnackBar(...), ColorScheme.fromSeed(...), ThemeData(...), ButtonSegment(...), DateTime(...), TimeOfDay(...), MaterialPageRoute(...), CupertinoPageRoute(...), RouteSettings(...), ValueKey(value), DataColumn(...), DataRow(...), DataCell(...), ExpansionPanel(...), Step(...), AnimationController(...), Tween(...), ColorTween(...), CurvedAnimation(...), PageRouteBuilder(...), SnackBarAction(...), RelativeRect.fromLTRB(...), FilledButton.tonal(...), RuneDataTableSource(...) |
| Constants | Colors.*, MainAxisAlignment.*, CrossAxisAlignment.*, MainAxisSize.*, TextAlign.*, TextOverflow.*, Alignment.*, BoxFit.*, StackFit.*, Axis.*, FontWeight.*, BoxShape.*, FlexFit.*, BottomNavigationBarType.*, CrossFadeState.*, Clip.*, DecorationPosition.*, ListTileControlAffinity.*, NavigationRailLabelType.*, Curves.linear/easeIn/easeOut/easeInOut/bounce*/elastic*/fastOutSlowIn, ~60 common Icons.*, ConnectionState.*, Orientation.*, SnackBarBehavior.*, ThemeMode.*, Brightness.*, MaterialTapTargetSize.*, AutovalidateMode.*, DismissDirection.*, StepperType.*, StepState.*, AnimationStatus.*, BorderStyle.*, TileMode.* |
| 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, Colors.grey[200] (any depth of nested maps plus list/map/MaterialColor indexing) |
| Collections | [for (final item in items) Text(item.title)] (data-driven widget lists, nested for-elements, static + for elements interleaved) |
| Built-in properties | .length, .isEmpty, .isNotEmpty, .first, .last on lists; .length, .isEmpty, .isNotEmpty on strings; .length, .isEmpty, .isNotEmpty, .keys, .values on maps; .hasData/.data/.hasError/.error/.connectionState on AsyncSnapshot; .maxWidth/.minWidth/.maxHeight/.minHeight/.biggest/.smallest on BoxConstraints; .alpha/.red/.green/.blue/.opacity/.value on Color; .inDays/.inHours/.inMinutes/.inSeconds/.inMilliseconds/.inMicroseconds on Duration; .year/.month/.day/.hour/.minute/.second/.weekday/.millisecondsSinceEpoch on DateTime; ThemeData, ColorScheme, TextTheme, MediaQueryData, Size, EdgeInsets property access (see CHANGELOG) |
| Built-in methods | toString() (any); toUpperCase/toLowerCase/trim/contains/startsWith/endsWith/split/substring/replaceAll on strings; contains/indexOf/join on lists plus closure-accepting map/where/any/every/firstWhere/forEach/fold/reduce; containsKey/containsValue on maps; abs/round/floor/ceil/toInt/toDouble on num; isBefore/isAfter/difference/add/subtract on DateTime |
| Events | ElevatedButton(onPressed: 'submit', ...) → RuneView.onEvent('submit', []) |
| Property extensions | 10.w, size.half (via RuneBridge packages registering handlers) |
| Operators | == != < <= > >= on num+num or String+String; && || (short-circuit); + - * / % on num; ! on bool; unary - on num |
| Conditionals | Ternary cond ? a : b; list-literal [if (cond) widget] / [if (cond) a else b] (both short-circuit the un-taken branch) |
| Stateful source | StatefulBuilder(initial: {...}, builder: (state) => ...) produces source-level state; state.key reads, state.key = value assigns, setState(() { ... }) wraps a batch of mutations. Optional initState / dispose / didUpdateWidget closures own the full mount / unmount / rebuild lifecycle; autoDisposeListenables: true disposes any ChangeNotifier entries automatically on unmount. |
| Components | RuneComponent(name: 'X', params: [...], body: (...) => ...) declares a reusable component; RuneCompose(components: [...], root: ...) groups declarations and the widget tree; components dispatch before widget/value registries. |
| Imperative bridges | showDialog(builder: ...), showModalBottomSheet(builder: ...), showSnackBar(snackBar), Navigator.pop(result?), showDatePicker(initialDate, firstDate, lastDate), showTimePicker(initialTime), Navigator.push(route), Navigator.pushReplacement(route), Navigator.pushNamed(name, arguments?), Navigator.canPop(), Navigator.popUntil(predicate), showMenu(position, items, ...). All route through RuneContext.flutterContext. |
| Context accessors | Theme.of(context), MediaQuery.of(context). Return raw Flutter values with whitelisted property access. |
| Developer utilities | formatRuneSource(source) canonical formatter; SourceSpan.toContextualPointer(source, contextLines) widened error pointer; "did you mean X?" suggestions on missing builder / method / identifier diagnostics. |
| Sibling bridges | Cupertino widgets via rune_cupertino; ChangeNotifierProvider / Consumer / Selector via rune_provider; GoRoute / GoRouter / GoRouterApp via rune_router; .w / .h / .sp / .dm responsive extensions via rune_responsive_sizer (see Bridge packages section below). |
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.
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.
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());config.constants
..register('BrandTheme', 'primary', const Color(0xFF0088FF))
..register('BrandTheme', 'accent', const Color(0xFFFF6B35));Source strings can then use Container(color: BrandTheme.primary, ...).
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.
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.
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.
Every RuneException carries an optional location field: a SourceSpan pointing into the RuneView.source where the error originates. When present, toString() renders a caret pointer beneath the one-line summary:
ResolveException: Unknown identifier "userNmae" (not present in RuneDataContext) (source: "userNmae")
at line 2, column 9:
Text(userNmae)
^^^^^^^^
Access the structured data programmatically for custom diagnostics UI:
RuneView(
source: mySource,
onError: (error, _) {
if (error is RuneException) {
final loc = error.location;
if (loc != null) {
debugPrint('Rune error at L${loc.line}:C${loc.column}: ${error.message}');
debugPrint(loc.toPointerString());
}
}
},
);Locations are populated for parse errors (analyzer diagnostics with offsets), every resolver throw site (via the AST node's offset/length), and bubbled builder ArgumentExceptions (rewrapped at the invocation). They are null for defensive throw sites that have no user-visible offset (e.g., the wrapped-variable-had-no-initializer invariant check inside DartParser), so consumers should treat the field as optional.
flutter test
flutter analyze1738 root tests plus 163 sibling-package tests (7 in rune_responsive_sizer, 117 in rune_cupertino, 19 in rune_provider, 20 in rune_router) cover every resolver, every builder, every registry, the architecture invariants, and end-to-end RuneView renders. Main is kept green at all times; every commit passes both gates under very_good_analysis ^5.1.0, and CI runs the full matrix on every push across Flutter 3.24.0 (pinned floor) and the latest stable channel.
Run the bundled microbenchmark to measure parse-plus-resolve latency on your machine:
flutter test benchmark/parse_resolve_bench.dartNumbers captured on an Apple-Silicon development machine, v1.17.1:
| Source | COLD p95 (parse + resolve) | WARM p95 (resolve only) | Headroom vs. 16ms 60fps budget |
|---|---|---|---|
| Canonical 30-node tree | 450us | 50us | ~36x |
Rich source (interpolation, for / if elements, deep dot-paths) |
410us | 121us | ~39x |
COLD is a cache-miss: fresh parse plus full resolver walk every iteration. WARM is a cache-hit: AstCache returns the pre-parsed tree so only the resolver runs. Both paths comfortably clear a 60fps budget at realistic source sizes. Your numbers will differ by device class; rerun locally if you need representative figures for a target hardware tier.
See example/ for a runnable 4-tab demo that exercises the full current feature set including rune_provider and rune_responsive_sizer.
Common recipes for shaping Rune source to solve real problems. Copy, paste, adapt.
RuneView(
data: {'username': username},
source: r"""
TextField(
value: username,
onChanged: 'usernameChanged',
labelText: 'Username',
)
""",
onEvent: (name, [args]) {
if (name == 'usernameChanged') {
setState(() => username = args!.first as String);
}
},
)The value: arg is read on every rebuild; the onChanged: event dispatches the new text. Same pattern works for Switch(value:, onChanged:), Checkbox(value:, onChanged:), and Slider(value:, onChanged:).
Column(children: [
if (cart.items.isEmpty) Text('Your cart is empty.'),
if (cart.items.isNotEmpty)
for (final item in cart.items) ListTile(title: Text(item.name)),
if (cart.items.length >= 3) Text('Free shipping unlocked!'),
])
if-elements short-circuit cleanly inside Column.children / Row.children. Use if (a) X else Y for either-or branches.
ElevatedButton(
onPressed: username.isEmpty ? 'noop' : 'save',
child: Text('Save'),
)
Route the same button to a no-op event until a predicate is satisfied. The button stays visually enabled; the host simply ignores 'noop'.
Host defines a ChangeNotifier that also implements RuneReactiveNotifier:
class CounterNotifier extends ChangeNotifier
implements RuneReactiveNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count += 1;
notifyListeners();
}
@override
Map<String, Object?> get state => {'count': _count};
}Source consumes it through the ProviderBridge:
ChangeNotifierProvider(
value: counter,
child: Consumer(
builder: (ctx, state, child) => Text('Count: ${state.count}'),
),
)
The Map-shaped state getter lets Rune's property resolver reach individual fields via ordinary dot-access.
final config = RuneConfig.defaults()
.withBridges(const [ResponsiveSizerBridge()]);Source uses .w / .h / .sp extensions on num literals:
Container(
width: 80.w,
height: 8.h,
child: Text('Hi', style: TextStyle(fontSize: 16.sp)),
)
Declare routes inline; mount through GoRouterApp:
GoRouterApp(
router: GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (ctx, state) => Scaffold(
body: Center(child: Text('Home')),
),
),
GoRoute(
path: '/settings',
builder: (ctx, state) => Scaffold(
body: Center(child: Text('Settings')),
),
),
],
),
)
Navigate host-side by holding a reference to the GoRouter: router.go('/settings').
A RuneBridge is one class with one method. Everything else is ordinary Flutter.
packages/my_bridge/
pubspec.yaml # depends on rune: path: ../..
analysis_options.yaml
lib/
my_bridge.dart # barrel: export 'src/my_bridge_impl.dart' show MyBridge;
src/
my_bridge_impl.dart
widgets/
my_widget_builder.dart
test/
my_bridge_test.dart
import 'package:rune/rune.dart';
final class MyBridge implements RuneBridge {
const MyBridge();
@override
void registerInto(RuneConfig config) {
config.widgets.registerBuilder(const MyWidgetBuilder());
config.values.registerBuilder(const MyValueBuilder());
config.constants.registerAll('MyConstants', {
'green': MyColors.green,
'red': MyColors.red,
});
config.extensions.register('percent', (target, ctx) {
if (target is num) return '$target%';
throw ArgumentError('.percent expects num');
});
}
}final class MyWidgetBuilder implements RuneWidgetBuilder {
const MyWidgetBuilder();
@override
String get typeName => 'MyWidget';
@override
Widget build(ResolvedArguments args, RuneContext ctx) {
return MyWidget(
title: args.require<String>('title', source: 'MyWidget'),
color: args.get<Color>('color'),
);
}
}For closure-accepting slots (builder:, onPressed: with a closure body, etc.), see packages/rune_cupertino/lib/src/widgets/cupertino_tab_scaffold_builder.dart for the canonical pattern that imports RuneClosure through a narrowly-suppressed implementation_imports.
final config = RuneConfig.defaults()
.withBridges(const [MyBridge()]);
RuneView(config: config, source: "MyWidget(title: 'Hi')");The four sibling bridges in packages/ are live reference implementations. Start from rune_responsive_sizer (smallest, extension-only), then rune_cupertino (widget-heavy), then rune_provider (closure-heavy), then rune_router (value-builder-heavy).
Third-party and first-party integrations ship as separate bridge
packages that register widgets, values, constants, and extensions
into a shared RuneConfig. Each package has its own version track
and README.
| Package | Description |
|---|---|
rune_responsive_sizer |
Percent-of-screen extensions: .w, .h, .sp, .dm. |
rune_cupertino |
Cupertino widget family (CupertinoApp through CupertinoAlertDialog), CupertinoThemeData, CupertinoIcons constants. |
rune_provider |
Reactive state from package:provider: ChangeNotifierProvider, Consumer, Selector. Notifiers expose Map-shaped state via a RuneReactiveNotifier.state getter. |
rune_router |
Inline routing via package:go_router: GoRoute, GoRouter, GoRouterApp (wraps MaterialApp.router). |
rune_devtools_extension |
Flutter DevTools extension. Adds a rune tab that inspects every live RuneView (source, data context, parse-cache size, last error) via the ext.rune.inspect VM service endpoint. |
rune_http |
RuneHttpView: fetch Rune source from a URL, cache in memory with TTL, offline-first fallback to the last-known-good copy. Unlocks the server-driven-UI use case end-to-end. |
rune_bloc |
BLoC-pattern state from package:flutter_bloc: BlocProvider, BlocBuilder, BlocListener. State classes implement RuneReactiveState to expose Map-shaped projections. |
rune_riverpod |
Riverpod 2.x integration: ProviderScope, RiverpodConsumer. Typed state classes implement RuneReactiveValue for dot-access. |
rune_lint |
Test-time validation: expectValidRuneSource(tester, source, config) catches unregistered widgets, missing constants, typos, and parse errors before a user ever sees the fallback widget. |
rune_test |
Widget-test helpers (pumpRuneView, expectRuneRenders) plus a rune_format CLI that wraps formatRuneSource (--write, --check, stdin, custom line length). |
Apply any bridge with RuneConfig.defaults().withBridges([...]).
The RuneBridge contract is one method: void registerInto(RuneConfig config).
MIT. See LICENSE.
