rfw_gen
A code generation package that converts Flutter Widget code into
Remote Flutter Widgets (RFW) format.
Annotate top-level functions with @RfwWidget, run build_runner, and get
.rfwtxt and .rfw files ready for server-driven UI.
Installation
Add rfw_gen to your dependencies and rfw_gen_builder + build_runner
to your dev dependencies:
dependencies:
rfw: ^1.0.17
rfw_gen: ^0.4.0
dev_dependencies:
rfw_gen_builder: ^0.4.0
build_runner: ^2.4.0
Note: The
rfwpackage is required at runtime to render Remote Flutter Widgets.rfw_genandrfw_gen_builderhandle code generation only.
Quick Start
- Write a widget function and annotate it with
@RfwWidget:
import 'package:flutter/material.dart';
import 'package:rfw_gen/rfw_gen.dart';
@RfwWidget('greeting')
Widget buildGreeting() {
return Container(
color: Color(0xFF2196F3),
child: Text('Hello, RFW!'),
);
}
- Run the code generator:
dart run build_runner build
- The generator produces two files:
greeting.rfwtxt-- human-readable RFW text formatgreeting.rfw-- binary format for production use
Supported Widgets
65 widgets are supported across two categories.
Core Widgets (~48)
- Layout: Align, AspectRatio, Center, Column, Expanded, Flexible, FittedBox, FractionallySizedBox, IntrinsicHeight, IntrinsicWidth, Row, SizedBox, SizedBoxExpand, SizedBoxShrink, Spacer, Stack, Wrap
- Scrolling: GridView, ListBody, ListView, SingleChildScrollView
- Styling: ClipRRect, ColoredBox, Container, DefaultTextStyle, Directionality, Icon, IconTheme, Image, Opacity, Padding, Placeholder, Text
- Transform: Positioned, Rotation, Scale
- Interaction: GestureDetector
- Other: AnimationDefaults, SafeArea
Material Widgets (~17)
AppBar, Card, CircularProgressIndicator, Divider, Drawer, ElevatedButton, FloatingActionButton, InkWell, LinearProgressIndicator, ListTile, Material, OutlinedButton, OverflowBar, Scaffold, Slider, TextButton, VerticalDivider
See rules/rfw-widgets.md
for the full parameter reference.
Dynamic Features
Data Binding
Reference server-supplied data with DataRef:
Text(DataRef('user.name'))
Generates: Text(text: data.user.name)
Args
Reference widget constructor arguments with ArgsRef:
Text(ArgsRef('product.id'))
Generates: Text(text: args.product.id)
State
Declare local widget state and reference it with StateRef:
@RfwWidget('toggle', state: {'isActive': false})
Widget buildToggle() {
return Container(
color: RfwSwitchValue(
value: StateRef('isActive'),
cases: {true: Color(0xFF4CAF50), false: Color(0xFFE0E0E0)},
),
);
}
Loops
Iterate over lists with RfwFor:
RfwFor(
items: DataRef('items'),
itemName: 'item',
builder: (item) => ListTile(
title: Text(item['name']),
),
)
Generates: ...for item in data.items: ListTile(title: Text(text: item.name))
Conditionals
Use RfwSwitch for child widgets and RfwSwitchValue for values:
RfwSwitch(
value: StateRef('status'),
cases: {
'loading': CircularProgressIndicator(),
'done': Text('Complete'),
},
)
String Concatenation
Combine static text and dynamic references with RfwConcat:
Text(RfwConcat(['Hello, ', DataRef('name'), '!']))
Generates: Text(text: ["Hello, ", data.name, "!"])
Event Handlers
Mutate local state:
GestureDetector(
onTap: RfwHandler.setState('pressed', true),
)
Generates: onTap: set state.pressed = true
Dispatch events to the Flutter host:
GestureDetector(
onTap: RfwHandler.event('cart.add', {'itemId': 42}),
)
Generates: onTap: event "cart.add" { itemId: 42 }
Toggle a boolean state value using RfwSwitchValue:
@RfwWidget('toggle', state: {'isActive': false})
Widget buildToggle() {
return GestureDetector(
onTap: RfwHandler.setState('isActive', RfwSwitchValue<bool>(
value: StateRef('isActive'),
cases: {true: false, false: true},
)),
child: Container(
color: RfwSwitchValue<int>(
value: StateRef('isActive'),
cases: {true: 0xFF4CAF50, false: 0xFFE0E0E0},
),
),
);
}
Generates:
onTap: set state.isActive = switch state.isActive {
true: false,
false: true,
},
Custom Widgets
Custom widgets are automatically detected and supported. When you use a
non-built-in widget inside an @RfwWidget function, the generator:
- Discovers the custom widget via AST analysis
- Resolves its constructor using the Dart analyzer (WidgetResolver)
- Generates a
LocalWidgetBuilderbridge (.rfw_library.dart) - Emits widget metadata (
.rfw_meta.json) for MCP/tooling
// 1. Define your custom widget
class CustomText extends StatelessWidget {
const CustomText({super.key, required this.text, this.fontSize = 14.0});
final String text;
final double fontSize;
@override
Widget build(BuildContext context) => Text(text, style: TextStyle(fontSize: fontSize));
}
// 2. Use it in an @RfwWidget function — no config file needed
@RfwWidget('customDemo')
Widget buildCustomDemo() {
return CustomText(text: 'Hello', fontSize: 24.0);
}
Running build_runner generates:
custom_demo.rfwtxt/.rfw— RFW output withCustomTextreferences.rfw_library.dart—Map<String, LocalWidgetBuilder>that wires RFW'sDataSourceto your widget's constructor.rfw_meta.json— machine-readable parameter/child/handler metadata
No rfw_gen.yaml configuration is required (removed in 0.4.0).
Generated Output
For each .dart file containing @RfwWidget functions, the generator produces:
| File | Purpose |
|---|---|
.rfwtxt |
Human-readable RFW text format |
.rfw |
Binary format for production use |
.rfw_library.dart |
LocalWidgetBuilder map for custom widgets |
.rfw_meta.json |
Widget metadata (params, child type, handlers) |
Rendering Generated Widgets
After running build_runner, use the generated .rfw file with the
rfw package to render widgets:
import 'package:flutter/material.dart';
import 'package:rfw/rfw.dart';
class RfwScreen extends StatefulWidget {
const RfwScreen({super.key});
@override
State<RfwScreen> createState() => _RfwScreenState();
}
class _RfwScreenState extends State<RfwScreen> {
final Runtime _runtime = Runtime();
final DynamicContent _data = DynamicContent();
@override
void initState() {
super.initState();
// Register built-in widget libraries
_runtime.update(const LibraryName(<String>['core', 'widgets']), createCoreWidgets);
_runtime.update(const LibraryName(<String>['material']), createMaterialWidgets);
// Load the generated .rfw binary
rootBundle.load('assets/greeting.rfw').then((bytes) {
_runtime.update(
const LibraryName(<String>['main']),
decodeLibraryBlob(bytes.buffer.asUint8List()),
);
setState(() {});
});
// Supply dynamic data
_data.update('user', <String, Object>{'name': 'World'});
}
@override
Widget build(BuildContext context) {
return RemoteWidget(
runtime: _runtime,
data: _data,
widget: const FullyQualifiedWidgetName(
LibraryName(<String>['main']),
'greeting',
),
);
}
}
For more details, see the rfw package documentation.
Limitations
-
@RfwWidgetmust be applied to top-level functions only. -
Only the 65 built-in widgets (core + material) are supported out of the box. Other widgets are auto-detected and require their source to be importable.
-
RfwSwitchValue/RfwSwitchcannot be used in handler positions (e.g.,onTap,onPressed). This is an RFW runtime limitation — handlers only supportsetState,setStateFromArg, andevent. To conditionally switch behavior, useRfwSwitchto swap entire widgets:// ❌ This does NOT work: onPressed: RfwSwitchValue(value: StateRef('mode'), cases: { true: RfwHandler.event('pause', {}), false: RfwHandler.event('start', {}), }) // ✅ Use RfwSwitch to swap widgets instead: RfwSwitch( value: StateRef('mode'), cases: { true: ElevatedButton( onPressed: RfwHandler.event('pause', {}), child: Text('Pause'), ), false: ElevatedButton( onPressed: RfwHandler.event('start', {}), child: Text('Start'), ), }, )
Pre-1.0 Note
This package is pre-1.0. Minor version bumps may include breaking changes. Pin to a specific version if stability is critical for your project.