Skip to content

Instantly share code, notes, and snippets.

@rodydavis
Last active May 5, 2025 17:50
Show Gist options
  • Save rodydavis/b376ea1c3d834af8b1b2416719b9549b to your computer and use it in GitHub Desktop.
Save rodydavis/b376ea1c3d834af8b1b2416719b9549b to your computer and use it in GitHub Desktop.
RFW LLMs.txt

Core Widgets for Remote Flutter Widgets (RFW)

This document outlines the core widgets available for use with Remote Flutter Widgets (RFW), detailing their supported properties.

Important Considerations:

  • Enums are represented as strings (e.g., "start" for MainAxisAlignment.start).
  • Types with multiple subclasses (e.g., ColorFilter) are represented as maps with a type key.
  • Matrices are represented as column-major flattened arrays.
  • AlignmentGeometry can be {x: ..., y: ...} or {start: ..., y: ...}.
  • BoxBorder and BorderRadiusGeometry values are defined as arrays, with lengths affecting how sides/corners are assigned.
  • Color values are represented as integers (0xAARRGGBB).
  • Duration is represented by an integer giving milliseconds.
  • ScrollController and ScrollPhysics are not supported.
  • Image does not support builder callbacks or Image.opacity parameter. The map should have a source key that is interpreted as described above for DecorationImage. If the source is omitted, an AssetImage with the name error.png is used instead.
  • Text widget's first argument is represented using the key text, which must be either a string or an array of strings.

Widget List

1. AnimationDefaults

Sets default animation duration and curve for other animated widgets.

  • Required Properties:
    • child: Widget
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve

2. Align

A widget that aligns its child within itself.

  • Required Properties: None.
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve
    • alignment: AlignmentGeometry (defaults to Alignment.center)
    • widthFactor: double
    • heightFactor: double
    • child: Widget
    • onEnd: voidHandler

3. AspectRatio

A widget that attempts to size itself to match a given aspect ratio.

  • Required Properties: None.
  • Optional Properties:
    • aspectRatio: double (defaults to 1.0)
    • child: Widget

4. Center

A widget that centers its child within itself.

  • Required Properties: None.
  • Optional Properties:
    • widthFactor: double
    • heightFactor: double
    • child: Widget

5. ClipRRect

A widget that clips its child using a rounded rectangle.

  • Required Properties: None.
  • Optional Properties:
    • borderRadius: BorderRadius (defaults to BorderRadius.zero)
    • clipBehavior: Clip (defaults to Clip.antiAlias)
    • child: Widget

6. ColoredBox

A widget that paints its child with a given color.

  • Required Properties: None.
  • Optional Properties:
    • color: Color (defaults to 0xFF000000)
    • child: Widget

7. Column

A widget that displays its children in a vertical array.

  • Required Properties:
    • children: List
  • Optional Properties:
    • mainAxisAlignment: MainAxisAlignment (defaults to MainAxisAlignment.start)
    • mainAxisSize: MainAxisSize (defaults to MainAxisSize.max)
    • crossAxisAlignment: CrossAxisAlignment (defaults to CrossAxisAlignment.center)
    • textDirection: TextDirection
    • verticalDirection: VerticalDirection (defaults to VerticalDirection.down)
    • textBaseline: TextBaseline

8. Container

A convenience widget that combines common painting, positioning, and sizing widgets.

  • Required Properties: None.
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve
    • alignment: AlignmentGeometry
    • padding: EdgeInsets
    • color: Color
    • decoration: Decoration
    • foregroundDecoration: Decoration
    • width: double
    • height: double
    • constraints: BoxConstraints
    • margin: EdgeInsets
    • transform: Matrix4 (column-major flattened array with 16 doubles)
    • transformAlignment: AlignmentGeometry
    • clipBehavior: Clip (defaults to Clip.none)
    • child: Widget
    • onEnd: voidHandler

9. DefaultTextStyle

The text style to apply to descendant Text widgets without explicit styles.

  • Required Properties:
    • style: TextStyle
    • child: Widget
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve
    • textAlign: TextAlign
    • softWrap: bool (defaults to true)
    • overflow: TextOverflow (defaults to TextOverflow.clip)
    • maxLines: int
    • textWidthBasis: TextWidthBasis (defaults to TextWidthBasis.parent)
    • textHeightBehavior: TextHeightBehavior
    • onEnd: voidHandler

10. Directionality

Specifies the text direction for its child.

  • Required Properties:
    • textDirection: TextDirection (defaults to TextDirection.ltr)
    • child: Widget

11. Expanded

A widget that expands to fill the available space in a Row or Column.

  • Required Properties:
    • child: Widget
  • Optional Properties:
    • flex: int (defaults to 1)

12. FittedBox

Scales and positions its child within itself according to [fit].

  • Required Properties: None.
  • Optional Properties:
    • fit: BoxFit (defaults to BoxFit.contain)
    • alignment: AlignmentGeometry (defaults to Alignment.center)
    • clipBehavior: Clip (defaults to Clip.none)
    • child: Widget

13. FractionallySizedBox

A widget that sizes its child to a fraction of the total available space.

  • Required Properties:
    • child: Widget
  • Optional Properties:
    • alignment: AlignmentGeometry (defaults to Alignment.center)
    • widthFactor: double
    • heightFactor: double

14. GestureDetector

A widget that detects gestures.

  • Required Properties: None.
  • Optional Properties:
    • onTap: voidHandler
    • onTapDown: Function(TapDownDetails) handler
    • onTapUp: Function(TapUpDetails) handler
    • onTapCancel: voidHandler
    • onDoubleTap: voidHandler
    • onLongPress: voidHandler
    • behavior: HitTestBehavior
    • child: Widget

15. GridView

A scrollable, 2D array of widgets. (Uses GridView.builder internally).

  • Required Properties:
    • children: List
  • Optional Properties:
    • scrollDirection: Axis (defaults to Axis.vertical)
    • reverse: bool (defaults to false)
    • primary: bool
    • shrinkWrap: bool (defaults to false)
    • padding: EdgeInsets
    • gridDelegate: GridDelegate (defaults to SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2))
    • addAutomaticKeepAlives: bool (defaults to true)
    • addRepaintBoundaries: bool (defaults to true)
    • addSemanticIndexes: bool (defaults to true)
    • cacheExtent: double
    • semanticChildCount: int
    • dragStartBehavior: DragStartBehavior (defaults to DragStartBehavior.start)
    • keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior (defaults to ScrollViewKeyboardDismissBehavior.manual)
    • restorationId: String
    • clipBehavior: Clip (defaults to Clip.hardEdge)

16. Icon

A graphical icon widget, drawn with a font.

  • Required Properties: None.
  • Optional Properties:
    • size: double
    • color: Color
    • semanticLabel: String
    • textDirection: TextDirection
    • icon: IconData

17. IconTheme

Applies an [IconThemeData] to descendant widgets.

  • Required Properties:
    • child: Widget
  • Optional Properties:
    • data: IconThemeData

18. IntrinsicHeight

A widget that sizes its child to the child's intrinsic height.

  • Required Properties:
    • child: Widget

19. IntrinsicWidth

A widget that sizes its child to the child's intrinsic width.

  • Required Properties:
    • child: Widget
  • Optional Properties:
    • width: double
    • height: double

20. Image

A widget that displays an image.

  • Required Properties: None.
  • Optional Properties:
    • image: ImageProvider
    • semanticLabel: String
    • excludeFromSemantics: bool (defaults to false)
    • width: double
    • height: double
    • color: Color
    • blendMode: BlendMode
    • fit: BoxFit
    • alignment: AlignmentGeometry (defaults to Alignment.center)
    • repeat: ImageRepeat (defaults to ImageRepeat.noRepeat)
    • centerSlice: Rect (array of 4 doubles: x, y, width, height)
    • matchTextDirection: bool (defaults to false)
    • gaplessPlayback: bool (defaults to false)
    • isAntiAlias: bool (defaults to false)
    • filterQuality: FilterQuality (defaults to FilterQuality.low)
    • source: (For DecorationImage, either absolute URL or asset name, used to construct AssetImage if URL is missing)

21. ListBody

A widget that arranges its children sequentially along a given axis.

  • Required Properties:
    • children: List
  • Optional Properties:
    • mainAxis: Axis (defaults to Axis.vertical)
    • reverse: bool (defaults to false)

22. ListView

A scrollable list of widgets arranged linearly. (Uses ListView.builder internally).

  • Required Properties:
    • children: List
  • Optional Properties:
    • scrollDirection: Axis (defaults to Axis.vertical)
    • reverse: bool (defaults to false)
    • primary: bool
    • shrinkWrap: bool (defaults to false)
    • padding: EdgeInsets
    • itemExtent: double
    • prototypeItem: Widget
    • clipBehavior: Clip (defaults to Clip.hardEdge)
    • addAutomaticKeepAlives: bool (defaults to true)
    • addRepaintBoundaries: bool (defaults to true)
    • addSemanticIndexes: bool (defaults to true)
    • cacheExtent: double
    • semanticChildCount: int
    • dragStartBehavior: DragStartBehavior (defaults to DragStartBehavior.start)
    • keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior (defaults to ScrollViewKeyboardDismissBehavior.manual)
    • restorationId: String

23. Opacity

A widget that makes its child partially transparent.

  • Required Properties: None.
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve
    • opacity: double (defaults to 0.0)
    • alwaysIncludeSemantics: bool (defaults to true)
    • child: Widget
    • onEnd: voidHandler

24. Padding

A widget that insets its child by the given padding.

  • Required Properties: None.
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve
    • padding: EdgeInsets (defaults to EdgeInsets.zero)
    • child: Widget
    • onEnd: voidHandler

25. Placeholder

A widget that draws a box that indicates where other widgets will one day live.

  • Required Properties: None.
  • Optional Properties:
    • color: Color (defaults to 0xFF455A64)
    • strokeWidth: double (defaults to 2.0)
    • placeholderWidth: double (defaults to 400.0)
    • placeholderHeight: double (defaults to 400.0)

26. Positioned

A widget that controls where a child of a Stack is positioned.

  • Required Properties:
    • child: Widget
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve
    • start: double
    • top: double
    • end: double
    • bottom: double
    • width: double
    • height: double
    • onEnd: voidHandler

27. Rotation

A widget that rotates its child by a number of turns.

  • Required Properties: None.
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve
    • turns: double (defaults to 0.0)
    • alignment: AlignmentGeometry (defaults to Alignment.center)
    • filterQuality: FilterQuality
    • child: Widget
    • onEnd: voidHandler

28. Row

A widget that displays its children in a horizontal array.

  • Required Properties:
    • children: List
  • Optional Properties:
    • mainAxisAlignment: MainAxisAlignment (defaults to MainAxisAlignment.start)
    • mainAxisSize: MainAxisSize (defaults to MainAxisSize.max)
    • crossAxisAlignment: CrossAxisAlignment (defaults to CrossAxisAlignment.center)
    • textDirection: TextDirection
    • verticalDirection: VerticalDirection (defaults to VerticalDirection.down)
    • textBaseline: TextBaseline

29. SafeArea

A widget that insets its child by sufficient padding to avoid intrusions by the operating system.

  • Required Properties:
    • child: Widget
  • Optional Properties:
    • left: bool (defaults to true)
    • top: bool (defaults to true)
    • right: bool (defaults to true)
    • bottom: bool (defaults to true)
    • minimum: EdgeInsets (defaults to EdgeInsets.zero)
    • maintainBottomViewPadding: bool (defaults to false)

30. Scale

A widget that applies a scale transformation to its child.

  • Required Properties: None.
  • Optional Properties:
    • duration: Duration (milliseconds)
    • curve: Curve
    • scale: double (defaults to 1.0)
    • alignment: AlignmentGeometry (defaults to Alignment.center)
    • filterQuality: FilterQuality
    • child: Widget
    • onEnd: voidHandler

31. SingleChildScrollView

A box in which a single widget can be scrolled.

  • Required Properties: None.
  • Optional Properties:
    • scrollDirection: Axis (defaults to Axis.vertical)
    • reverse: bool (defaults to false)
    • padding: EdgeInsets
    • primary: bool (defaults to true)
    • dragStartBehavior: DragStartBehavior (defaults to DragStartBehavior.start)
    • clipBehavior: Clip (defaults to Clip.hardEdge)
    • restorationId: String
    • keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior (defaults to ScrollViewKeyboardDismissBehavior.manual)
    • child: Widget

32. SizedBox

A box with a specified size.

  • Required Properties: None.
  • Optional Properties:
    • width: double
    • height: double
    • child: Widget

33. SizedBoxExpand

A box that expands to fill available space.

  • Required Properties: None.
  • Optional Properties:
    • child: Widget

34. SizedBoxShrink

A box that shrink wraps to its child.

  • Required Properties: None.
  • Optional Properties:
    • child: Widget

35. Spacer

Creates an adjustable, empty space that can be used to tune the spacing between widgets in a Flex container such as a Row or Column.

  • Required Properties: None.
  • Optional Properties:
    • flex: int (defaults to 1)

36. Stack

A widget that positions its children relative to the edges of its box.

  • Required Properties:
    • children: List
  • Optional Properties:
    • alignment: AlignmentGeometry (defaults to AlignmentDirectional.topStart)
    • textDirection: TextDirection
    • fit: StackFit (defaults to StackFit.loose)
    • clipBehavior: Clip (defaults to Clip.hardEdge)

37. Text

A widget that displays a string of text.

  • Required Properties:
    • text: String | List
  • Optional Properties:
    • style: TextStyle
    • strutStyle: StrutStyle
    • textAlign: TextAlign
    • textDirection: TextDirection
    • locale: Locale
    • softWrap: bool
    • overflow: TextOverflow
    • textScaler: TextScaler
    • maxLines: int
    • semanticsLabel: String
    • textWidthBasis: TextWidthBasis
    • textHeightBehavior: TextHeightBehavior

38. Wrap

A widget that displays its children in multiple horizontal or vertical runs.

  • Required Properties:
    • children: List
  • Optional Properties:
    • direction: Axis (defaults to Axis.horizontal)
    • alignment: WrapAlignment (defaults to WrapAlignment.start)
    • spacing: double (defaults to 0.0)
    • runAlignment: WrapAlignment (defaults to WrapAlignment.start)
    • runSpacing: double (defaults to 0.0)
    • crossAxisAlignment: WrapCrossAlignment (defaults to WrapCrossAlignment.start)
    • textDirection: TextDirection
    • verticalDirection: VerticalDirection (defaults to VerticalDirection.down)
    • clipBehavior: Clip (defaults to Clip.none)

Material Widgets for Remote Flutter Widgets (RFW)

This document outlines the Material widgets available for use with Remote Flutter Widgets (RFW), detailing their supported properties. Note that some features of the underlying Flutter Material library are not supported by RFW, as noted in the code's documentation.

Important Considerations:

  • Theming is generally not supported.
  • Properties whose values are Animations or based on WidgetStateProperty are not supported.
  • Features related to focus or configuring mouse support are not implemented.
  • Callbacks (like Scaffold.onDrawerChanged) are not exposed.
  • AppBar.bottom, AppBar.flexibleSpace, and AppBar.systemOverlayStyle are not supported.
  • The floatingActionButtonLocation and floatingActionButtonAnimator for Scaffold are not supported.

Widget List

1. AboutListTile

Displays an AboutDialog when tapped.

  • Required Properties: None.
  • Optional Properties:
    • icon: Widget
    • applicationName: String
    • applicationVersion: String
    • applicationIcon: Widget
    • applicationLegalese: String
    • aboutBoxChildren: List
    • dense: bool
    • child: Widget

2. AppBar

A material design app bar.

  • Required Properties: None.
  • Optional Properties:
    • leading: Widget
    • automaticallyImplyLeading: bool (defaults to true)
    • title: Widget
    • actions: List
    • elevation: double
    • shadowColor: Color
    • shape: ShapeBorder
    • backgroundColor: Color
    • foregroundColor: Color
    • iconTheme: IconThemeData
    • actionsIconTheme: IconThemeData
    • primary: bool (defaults to true)
    • centerTitle: bool
    • excludeHeaderSemantics: bool (defaults to false)
    • titleSpacing: double
    • toolbarOpacity: double (defaults to 1.0)
    • toolbarHeight: double
    • leadingWidth: double
    • toolbarTextStyle: TextStyle
    • titleTextStyle: TextStyle

3. ButtonBar

A horizontal arrangement of buttons (implemented using OverflowBar for backward compatibility).

  • Required Properties: None.

  • Optional Properties:

    • alignment: MainAxisAlignment (defaults to MainAxisAlignment.start)
    • buttonPadding: EdgeInsetsGeometry (defaults to EdgeInsets.all(8.0))
    • layoutBehavior: ButtonBarLayoutBehavior (defaults to ButtonBarLayoutBehavior.padded)
    • overflowDirection: VerticalDirection (defaults to VerticalDirection.down)
    • overflowButtonSpacing: double (defaults to 0.0)
    • children: List
    • mainAxisSize: MainAxisSize

    Note: buttonMinWidth, buttonHeight, and buttonAlignedDropdown are not supported.

4. OverflowBar

A layout that arranges its children in a row or column, and overflows if they don't fit.

  • Required Properties: None.
  • Optional Properties:
    • spacing: double (defaults to 0.0)
    • alignment: MainAxisAlignment
    • overflowSpacing: double (defaults to 0.0)
    • overflowAlignment: OverflowBarAlignment (defaults to OverflowBarAlignment.start)
    • overflowDirection: VerticalDirection (defaults to VerticalDirection.down)
    • textDirection: TextDirection
    • children: List

5. Card

A panel with rounded corners and a shadow.

  • Required Properties: None.
  • Optional Properties:
    • color: Color
    • shadowColor: Color
    • elevation: double
    • shape: ShapeBorder
    • borderOnForeground: bool (defaults to true)
    • margin: EdgeInsets
    • clipBehavior: Clip (defaults to Clip.none)
    • semanticContainer: bool (defaults to true)
    • child: Widget

6. CircularProgressIndicator

A circular indicator of progress.

  • Required Properties: None.
  • Optional Properties:
    • value: double
    • color: Color
    • backgroundColor: Color
    • strokeWidth: double (defaults to 4.0)
    • semanticsLabel: String
    • semanticsValue: String

7. Divider

A thin horizontal line, with padding on either side.

  • Required Properties: None.
  • Optional Properties:
    • height: double
    • thickness: double
    • indent: double
    • endIndent: double
    • color: Color

8. Drawer

A panel displayed to the side of the screen, often hidden.

  • Required Properties: None.
  • Optional Properties:
    • elevation: double (defaults to 16.0)
    • semanticLabel: String
    • child: Widget

9. DrawerHeader

A header for a drawer.

  • Required Properties: None.
  • Optional Properties:
    • duration: Duration
    • curve: Curve
    • decoration: Decoration
    • margin: EdgeInsets (defaults to EdgeInsets.only(bottom: 8.0))
    • padding: EdgeInsets (defaults to EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0))
    • child: Widget

10. DropdownButton

A button that displays a menu of items when pressed.

  • Required Properties:
    • items: A list of DropdownMenuItem configuration objects. Each object must contain child. Each object may also contain:
      • onTap: voidHandler
      • value: String | int | double | bool
      • enabled: bool
      • child: Widget
  • Optional Properties:
    • value: String | int | double | bool
    • disabledHint: Widget
    • onChanged: Function(value) handler
    • onTap: voidHandler
    • elevation: int (defaults to 8)
    • style: TextStyle
    • underline: Widget
    • icon: Widget
    • iconDisabledColor: Color
    • iconEnabledColor: Color
    • iconSize: double (defaults to 24.0)
    • isDense: bool (defaults to false)
    • isExpanded: bool (defaults to false)
    • itemHeight: double (defaults to kMinInteractiveDimension)
    • focusColor: Color
    • autofocus: bool (defaults to false)
    • dropdownColor: Color
    • menuMaxHeight: double
    • enableFeedback: bool
    • alignment: Alignment
    • borderRadius: BorderRadius
    • padding: EdgeInsets

11. ElevatedButton

A raised button with a filled background.

  • Required Properties:
    • onPressed: voidHandler
    • child: Widget
  • Optional Properties:
    • onLongPress: voidHandler
    • autofocus: bool (defaults to false)
    • clipBehavior: Clip (defaults to Clip.none)

12. FloatingActionButton

A floating action button, used for primary actions.

  • Required Properties:
    • onPressed: voidHandler
    • child: Widget
  • Optional Properties:
    • tooltip: String
    • foregroundColor: Color
    • backgroundColor: Color
    • focusColor: Color
    • hoverColor: Color
    • splashColor: Color
    • heroTag: String
    • elevation: double
    • focusElevation: double
    • hoverElevation: double
    • highlightElevation: double
    • disabledElevation: double
    • mini: bool (defaults to false)
    • shape: ShapeBorder
    • clipBehavior: Clip (defaults to Clip.none)
    • autofocus: bool (defaults to false)
    • materialTapTargetSize: MaterialTapTargetSize
    • isExtended: bool (defaults to false)
    • enableFeedback: bool

13. InkResponse

A rectangular area of a Material that responds to touch.

  • Required Properties: None.
  • Optional Properties:
    • onTap: voidHandler
    • onTapDown: Function(TapDownDetails) handler
    • onTapUp: Function(TapUpDetails) handler
    • onTapCancel: voidHandler
    • onDoubleTap: voidHandler
    • onLongPress: voidHandler
    • onSecondaryTap: voidHandler
    • onSecondaryTapUp: Function(TapUpDetails) handler
    • onSecondaryTapDown: Function(TapDownDetails) handler
    • onSecondaryTapCancel: voidHandler
    • onHighlightChanged: Function(bool highlighted) handler
    • onHover: Function(bool hovered) handler
    • containedInkWell: bool (defaults to false)
    • highlightShape: BoxShape (defaults to BoxShape.circle)
    • radius: double
    • borderRadius: BorderRadius
    • customBorder: ShapeBorder
    • focusColor: Color
    • hoverColor: Color
    • highlightColor: Color
    • splashColor: Color
    • enableFeedback: bool (defaults to true)
    • excludeFromSemantics: bool (defaults to false)
    • canRequestFocus: bool (defaults to true)
    • onFocusChange: Function(bool focus) handler
    • autofocus: bool (defaults to false)
    • hoverDuration: Duration
    • child: Widget

14. InkWell

A rectangular area of a Material that responds to touch (simpler than InkResponse).

  • Required Properties: None.
  • Optional Properties:
    • onTap: voidHandler
    • onDoubleTap: voidHandler
    • onLongPress: voidHandler
    • onTapDown: Function(TapDownDetails) handler
    • onTapCancel: voidHandler
    • onSecondaryTap: voidHandler
    • onSecondaryTapUp: Function(TapUpDetails) handler
    • onSecondaryTapDown: Function(TapDownDetails) handler
    • onSecondaryTapCancel: voidHandler
    • onHighlightChanged: Function(bool highlighted) handler
    • onHover: Function(bool hovered) handler
    • focusColor: Color
    • hoverColor: Color
    • highlightColor: Color
    • splashColor: Color
    • radius: double
    • borderRadius: BorderRadius
    • customBorder: ShapeBorder
    • enableFeedback: bool (defaults to true)
    • excludeFromSemantics: bool (defaults to false)
    • autofocus: bool (defaults to false)
    • child: Widget

15. LinearProgressIndicator

A linear indicator of progress.

  • Required Properties: None.
  • Optional Properties:
    • value: double
    • color: Color
    • backgroundColor: Color
    • minHeight: double
    • semanticsLabel: String
    • semanticsValue: String

16. ListTile

A single fixed-height row that typically contains some text as well as a leading or trailing icon.

  • Required Properties: None.
  • Optional Properties:
    • leading: Widget
    • title: Widget
    • subtitle: Widget
    • trailing: Widget
    • isThreeLine: bool (defaults to false)
    • dense: bool
    • visualDensity: VisualDensity
    • shape: ShapeBorder
    • contentPadding: EdgeInsets
    • enabled: bool (defaults to true)
    • onTap: voidHandler
    • onLongPress: voidHandler
    • selected: bool (defaults to false)
    • focusColor: Color
    • hoverColor: Color
    • autofocus: bool (defaults to false)
    • tileColor: Color
    • selectedTileColor: Color
    • enableFeedback: bool
    • horizontalTitleGap: double
    • minVerticalPadding: double
    • minLeadingWidth: double

17. Material

A container that provides a look and feel to its children based on the current theme.

  • Required Properties:
    • child: Widget
  • Optional Properties:
    • type: MaterialType (defaults to MaterialType.canvas)
    • elevation: double (defaults to 0.0)
    • color: Color
    • shadowColor: Color
    • surfaceTintColor: Color
    • textStyle: TextStyle
    • borderRadius: BorderRadius
    • shape: ShapeBorder
    • borderOnForeground: bool (defaults to true)
    • clipBehavior: Clip (defaults to Clip.none)
    • animationDuration: Duration

18. OutlinedButton

A button with an outlined border.

  • Required Properties:
    • onPressed: voidHandler
    • child: Widget
  • Optional Properties:
    • onLongPress: voidHandler
    • autofocus: bool (defaults to false)
    • clipBehavior: Clip (defaults to Clip.none)

19. Scaffold

Implements the basic material design visual layout structure.

  • Required Properties: None.
  • Optional Properties:
    • appBar: Widget
      • bottomHeight: double
    • body: Widget
    • floatingActionButton: Widget
    • persistentFooterButtons: List
    • drawer: Widget
    • endDrawer: Widget
    • bottomNavigationBar: Widget
    • bottomSheet: Widget
    • backgroundColor: Color
    • resizeToAvoidBottomInset: bool
    • primary: bool (defaults to true)
    • drawerDragStartBehavior: DragStartBehavior (defaults to DragStartBehavior.start)
    • extendBody: bool (defaults to false)
    • extendBodyBehindAppBar: bool (defaults to false)
    • drawerScrimColor: Color
    • drawerEdgeDragWidth: double
    • drawerEnableOpenDragGesture: bool (defaults to true)
    • endDrawerEnableOpenDragGesture: bool (defaults to true)
    • restorationId: String

20. Slider

A material design slider.

  • Required Properties:
    • onChanged: Function(double value) handler
    • value: double
  • Optional Properties:
    • secondaryTrackValue: double
    • onChangeStart: Function(double value) handler
    • onChangeEnd: Function(double value) handler
    • min: double (defaults to 0.0)
    • max: double (defaults to 1.0)
    • divisions: int
    • label: String
    • activeColor: Color
    • inactiveColor: Color
    • secondaryActiveColor: Color
    • thumbColor: Color
    • allowedInteraction: SliderInteraction

21. TextButton

A flat button with minimal styling.

  • Required Properties:
    • onPressed: voidHandler
    • child: Widget
  • Optional Properties:
    • onLongPress: voidHandler
    • autofocus: bool (defaults to false)
    • clipBehavior: Clip (defaults to Clip.none)

22. VerticalDivider

A thin vertical line, with padding on either side.

  • Required Properties: None.
  • Optional Properties:
    • width: double
    • thickness: double
    • indent: double
    • endIndent: double
    • color: Color

Remote Flutter Widgets

This package provides a mechanism for rendering widgets based on declarative UI descriptions that can be obtained at runtime.

Status

This package is relatively stable.

We plan to keep the format and supported widget set backwards compatible, so that once a file works, it will keep working. However, this is best-effort only. To guarantee that files keep working as you expect, submit tests to this package (e.g. the binary file and the corresponding screenshot, as a golden test).

The set of widgets supported by this package is somewhat arbitrary. PRs that add new widgets from Flutter's default widget libraries (widgets, material, and'cupertino) are welcome.

There are some known theoretical performance limitations with the package's current implementation, but so far nobody has reported experiencing them in production. Please file issues if you run into them.

Feedback

We would love to hear your experiences using this package, whether positive or negative. In particular, stories of uses of this package in production would be very interesting. Please add comments to issue 90218.

Limitations

Once you realize that you can ship UI (and maybe logic, e.g. using Wasm; see the example below) you will slowly be tempted to move your whole application to this model.

This won't work.

Flutter proper lets you create widgets for compelling UIs with gestures and animations and so forth. With RFW you can use those widgets, but it doesn't let you create those widgets.

For example, you don't want to use RFW to create a UI that involves page transitions. You don't want to use RFW to create new widgets that involve drag and drop. You don't want to use RFW to create widgets that involve custom painters.

Rather, RFW is best suited for interfaces made out of prebuilt components. For example, a database front-end could use this to describe bespoke UIs for editing different types of objects in the database. Message-of-the-day announcements could be built using this mechanism. Search interfaces could use this mechanism for rich result cards.

RFW is well-suited for describing custom UIs from a potentially infinite set of UIs that could not possibly have been known when the application was created. On the other hand, updating the application's look and feel, changing how navigation works in an application, or adding new features, are all changes that are best made in Flutter itself, creating a new application and shipping that through normal update channels.

Using Remote Flutter Widgets

Introduction

The Remote Flutter Widgets (RFW) package combines widget descriptions obtained at runtime, data obtained at runtime, some predefined widgets provided at compile time, and some app logic provided at compile time (possibly combined with other packages to enable new logic to be obtained at runtime), to generate arbitrary widget trees at runtime.

The widget descriptions obtained at runtime (e.g. over the network) are called remote widget libraries. These are normally transported in a binary format with the file extension .rfw. They can be written in a text format (file extension .rfwtxt), and either used directly or converted into the binary format for transport. The rfw package provides APIs for parsing and encoding these formats. The parts of the package that only deal with these formats can be imported directly and have no dependency on Flutter's dart:ui library, which means they can be used on the server or in command-line applications.

The data obtained at runtime is known as configuration data and is represented by DynamicContent objects. It uses a data structure similar to JSON (but it distinguishes int and double and does not support null). The rfw package provides both binary and text formats to carry this data; JSON can also be used directly (with some caution), and the data can be created directly in Dart. This is discussed in more detail in the DynamicContent API documentation.

Remote widget libraries can use the configuration data to define how the widgets are built.

Remote widget libraries all eventually bottom out in the predefined widgets that are compiled into the application. These are called local widget libraries. The rfw package ships with two local widget libraries, the core widgets from the widgets library (such as Text, Center, Row, etc), and some of the material widgets.

Programs can define their own local widget libraries, to provide more widgets for remote widget libraries to use.

These components are combined using a RemoteWidget widget and a Runtime object.

The remote widget libraries can specify events that trigger in response to callbacks. For example, the OutlinedButton widget defined in the Material local widget library has an onPressed property which the remote widget library can define as triggering an event. Events can contain data (either hardcoded or obtained from the configuration data).

These events result in a callback on the RemoteWidget being invoked. Events can either have hardcoded results, or the rfw package can be combined with other packages such as wasm_run_flutter so that events trigger code obtained at runtime. That code typically changes the configuration data, resulting in an update to the rendered widgets.

See also: API documentation

Getting started

A Flutter application can render remote widgets using the RemoteWidget widget, as in the following snippet:

class Example extends StatefulWidget {
  const Example({super.key});

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  final Runtime _runtime = Runtime();
  final DynamicContent _data = DynamicContent();

  // Normally this would be obtained dynamically, but for this example
  // we hard-code the "remote" widgets into the app.
  //
  // Also, normally we would decode this with [decodeLibraryBlob] rather than
  // parsing the text version using [parseLibraryFile]. However, to make it
  // easier to demo, this uses the slower text format.
  static final RemoteWidgetLibrary _remoteWidgets = parseLibraryFile('''
    // The "import" keyword is used to specify dependencies, in this case,
    // the built-in widgets that are added by initState below.
    import core.widgets;
    // The "widget" keyword is used to define a new widget constructor.
    // The "root" widget is specified as the one to render in the build
    // method below.
    widget root = Container(
      color: 0xFF002211,
      child: Center(
        child: Text(text: ["Hello, ", data.greet.name, "!"], textDirection: "ltr"),
      ),
    );
  ''');

  static const LibraryName coreName = LibraryName(<String>['core', 'widgets']);
  static const LibraryName mainName = LibraryName(<String>['main']);

  @override
  void initState() {
    super.initState();
    // Local widget library:
    _runtime.update(coreName, createCoreWidgets());
    // Remote widget library:
    _runtime.update(mainName, _remoteWidgets);
    // Configuration data:
    _data.update('greet', <String, Object>{'name': 'World'});
  }

  @override
  Widget build(BuildContext context) {
    return RemoteWidget(
      runtime: _runtime,
      data: _data,
      widget: const FullyQualifiedWidgetName(mainName, 'root'),
      onEvent: (String name, DynamicMap arguments) {
        // The example above does not have any way to trigger events, but if it
        // did, they would result in this callback being invoked.
        debugPrint('user triggered event "$name" with data: $arguments');
      },
    );
  }
}

In this example, the "remote" widgets are hardcoded into the application (_remoteWidgets), the configuration data is hardcoded and unchanging (_data), and the event handler merely prints a message to the console.

In typical usage, the remote widgets come from a server at runtime, either through HTTP or some other network transport. Separately, the DynamicContent data would be updated, either from the server or based on local data.

Similarly, events that are signalled by the user's interactions with the remote widgets (RemoteWidget.onEvent) would typically be sent to the server for the server to update the data, or would cause the data to be updated directly, on the user's device, according to some predefined logic.

It is recommended that servers send binary data, decoded using decodeLibraryBlob and decodeDataBlob, when providing updates for the remote widget libraries and data.

Applying these concepts to typical use cases

Message of the day, advertising, announcements

When rfw is used for displaying content that is largely static in presentation and updated only occasionally, the simplest approach is to encode everything into the remote widget library, download that to the client, and render it, with only minimal data provided in the configuration data (e.g. the user's dark mode preference, their username, the current date or time) and with a few predefined events (such as one to signal the message should be closed and another to signal the user checking a "do not show this again" checkbox, or similar).

Dynamic data editors

A more elaborate use case might involve remote widget libraries being used to describe the UI for editing structured data in a database. In this case, the data may be more important, containing the current data being edited, and the events may signal to the application how to update the data on the backend.

Search results

A general search engine could have dedicated remote widgets defined for different kinds of results, allowing the data to be formatted and made interactive in ways that are specific to the query and in ways that could not have been predicted when the application was created. For example, new kinds of search results for current events could be created on the fly and sent to the client without needing to update the client application.

Developing new local widget libraries

A "local" widget library is one that describes the built-in widgets that your "remote" widgets are built out of. The RFW package comes with some preprepared libraries, available through createCoreWidgets and createMaterialWidgets. You can also create your own.

When developing new local widget libraries, it is convenient to hook into the reassemble method to update the local widgets. That way, changes can be seen in real time when hot reloading.

class Example extends StatefulWidget {
  const Example({super.key});

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  final Runtime _runtime = Runtime();
  final DynamicContent _data = DynamicContent();

  @override
  void initState() {
    super.initState();
    _update();
  }

  @override
  void reassemble() {
    // This function causes the Runtime to be updated any time the app is
    // hot reloaded, so that changes to _createLocalWidgets can be seen
    // during development. This function has no effect in production.
    super.reassemble();
    _update();
  }

  static WidgetLibrary _createLocalWidgets() {
    return LocalWidgetLibrary(<String, LocalWidgetBuilder>{
      'GreenBox': (BuildContext context, DataSource source) {
        return ColoredBox(
          color: const Color(0xFF002211),
          child: source.child(<Object>['child']),
        );
      },
      'Hello': (BuildContext context, DataSource source) {
        return Center(
          child: Text(
            'Hello, ${source.v<String>(<Object>["name"])}!',
            textDirection: TextDirection.ltr,
          ),
        );
      },
    });
  }

  static const LibraryName localName = LibraryName(<String>['local']);
  static const LibraryName remoteName = LibraryName(<String>['remote']);

  void _update() {
    _runtime.update(localName, _createLocalWidgets());
    // Normally we would obtain the remote widget library in binary form from a
    // server, and decode it with [decodeLibraryBlob] rather than parsing the
    // text version using [parseLibraryFile]. However, to make it easier to
    // play with this sample, this uses the slower text format.
    _runtime.update(remoteName, parseLibraryFile('''
      import local;
      widget root = GreenBox(
        child: Hello(name: "World"),
      );
    '''));
  }

  @override
  Widget build(BuildContext context) {
    return RemoteWidget(
      runtime: _runtime,
      data: _data,
      widget: const FullyQualifiedWidgetName(remoteName, 'root'),
      onEvent: (String name, DynamicMap arguments) {
        debugPrint('user triggered event "$name" with data: $arguments');
      },
    );
  }
}

Widgets in local widget libraries are represented by closures that are invoked by the runtime whenever a local widget is referenced.

The closure uses the LocalWidgetBuilder signature. Like any builder in Flutter, it takes a BuildContext, which can be used to look up inherited widgets.

For example, widgets that need the current text direction might defer to Directionality.of(context), with the given BuildContext as the context argument.

The other argument is a DataSource. This gives access to the arguments that were provided to the widget in the remote widget library.

For example, consider the example above, where the remote widget library is:

import local;
widget root = GreenBox(
  child: Hello(name: "World"),
);

The GreenBox widget is invoked with one argument (child), and the Hello widget is invoked with one argument (name).

In the definitions of GreenBox and Hello, the data source is used to pull out these arguments.

Obtaining arguments from the DataSource

The arguments are a tree of maps and lists with leaves that are Dart scalar values (int, double, bool, or String), further widgets, or event handlers.

Scalars

Here is an example of a more elaborate widget argument:

widget fruit = Foo(
  bar: { quux: [ 'apple', 'banana', 'cherry' ] },
);

To obtain a scalar value from the arguments, the DataSource.v method is used. This method takes a list of keys (strings or integers) that denote the path to scalar in question. For instance, to obtain "cherry" from the example above, the keys would be bar, quux, and 2, as in:

'Foo': (BuildContext context, DataSource source) {
  return Text(source.v<String>(<Object>['bar', 'quux', 2])!);
},

The v method is generic, with a type argument that specifies the expected type (one of int, double, bool, or String). When the value of the argument in the remote widget library does not match the specified (or inferred) type given to v, or if the specified keys don't lead to a value at all, it returns null instead.

Maps and lists

The LocalWidgetBuilder callback can inspect keys to see if they are maps or lists before attempting to use them. For example, before accessing a dozen fields from a map, one might use isMap to check if the map is present at all. If it is not, then all the fields will be null, and it is inefficient to fetch each one individually.

The DataSource.isMap method is takes a list of keys (like v) and reports if the key identifies a map.

For example, in this case the bar argument can be treated either as a map with a name subkey, or a scalar String:

'Foo': (BuildContext context, DataSource source) {
  if (source.isMap(<Object>['bar'])) {
    return Text('${source.v<String>(<Object>['bar', 'name'])}', textDirection: TextDirection.ltr);
  }
  return Text('${source.v<String>(<Object>['bar'])}', textDirection: TextDirection.ltr);
},

Thus either of the following would have the same result:

widget example1 = GreenBox(
  child: Foo(
    bar: 'Jean',
  ),
);
widget example2 = GreenBox(
  child: Foo(
    bar: { name: 'Jean' },
  ),
);

The DataSource.isList method is similar but reports on whether the specified key identifies a list:

'Foo': (BuildContext context, DataSource source) {
  if (source.isList(<Object>['bar', 'quux'])) {
    return Text('${source.v<String>(<Object>['bar', 'quux', 2])}', textDirection: TextDirection.ltr);
  }
  return Text('${source.v<String>(<Object>['baz'])}', textDirection: TextDirection.ltr);
},

For lists, a LocalWidgetBuilder callback can iterate over the items in the list using the length method, which returns the length of the list (or zero if the key does not identify a list):

'Foo': (BuildContext context, DataSource source) {
  final int length = source.length(<Object>['text']);
  if (length > 0) {
    final StringBuffer text = StringBuffer();
    for (int index = 0; index < length; index += 1) {
      text.write(source.v<String>(<Object>['text', index]));
    }
    return Text(text.toString(), textDirection: TextDirection.ltr);
  }
  return const Text('<empty>', textDirection: TextDirection.ltr);
},

This could be used like this:

widget example3 = GreenBox(
  child: Foo(
    text: ['apple', 'banana']
  ),
);

Widgets

The GreenBox widget has a child widget, which is itself specified by the remote widget. This is common, for example, Row and Column widgets have children, Center has a child, and so on. Indeed, most widgets have children, except for those like Text, Image, and Spacer.

The GreenBox definition uses DataSource.child to obtain the widget, in a manner similar to the v method:

'GreenBox': (BuildContext context, DataSource source) {
  return ColoredBox(color: const Color(0xFF002211), child: source.child(<Object>['child']));
},

Rather than returning null when the specified key points to an argument that isn't a widget, the child method returns an ErrorWidget. For cases where having null is acceptable, the optionalChild method can be used:

'GreenBox': (BuildContext context, DataSource source) {
  return ColoredBox(color: const Color(0xFF002211), child: source.optionalChild(<Object>['child']));
},

It returns null when the specified key does not point to a widget.

For widgets that take lists of children, the childList method can be used. For example, this is how Row is defined in createCoreWidgets (see in particular the children line):

'Row': (BuildContext context, DataSource source) {
  return Row(
    mainAxisAlignment: ArgumentDecoders.enumValue<MainAxisAlignment>(MainAxisAlignment.values, source, ['mainAxisAlignment']) ?? MainAxisAlignment.start,
    mainAxisSize: ArgumentDecoders.enumValue<MainAxisSize>(MainAxisSize.values, source, ['mainAxisSize']) ?? MainAxisSize.max,
    crossAxisAlignment: ArgumentDecoders.enumValue<CrossAxisAlignment>(CrossAxisAlignment.values, source, ['crossAxisAlignment']) ?? CrossAxisAlignment.center,
    textDirection: ArgumentDecoders.enumValue<TextDirection>(TextDirection.values, source, ['textDirection']),
    verticalDirection: ArgumentDecoders.enumValue<VerticalDirection>(VerticalDirection.values, source, ['verticalDirection']) ?? VerticalDirection.down,
    textBaseline: ArgumentDecoders.enumValue<TextBaseline>(TextBaseline.values, source, ['textBaseline']),
    children: source.childList(['children']),
  );
},

ArgumentDecoders

It is common to need to decode types that are more structured than merely int, double, bool, or String scalars, for example, enums, Colors, or Paints.

The ArgumentDecoders namespace offers some utility functions to make the decoding of such values consistent.

For example, the Row definition above has some cases of enums. To decode them, it uses the ArgumentDecoders.enumValue method.

Handlers

The last kind of argument that widgets can have is callbacks.

Since remote widget libraries are declarative and not code, they cannot represent executable closures. Instead, they are represented as events. For example, here is how the "7" button from the calculator example is represented:

CalculatorButton(label: "7", onPressed: event "digit" { arguments: [7] }),

This creates a CalculatorButton widget with two arguments, label, a string, and onPressed, an event, whose name is "digit" and whose arguments are a map with one key, "arguments", whose value is a list with one value 7.

In that example, CalculatorButton is itself a remote widget that is defined in terms of a Button, and the onPressed argument is passed to the onPressed of the Button, like this:

widget CalculatorButton = Padding(
  padding: [8.0],
  child: SizedBox(
    width: 100.0,
    height: 100.0,
    child: Button(
      child: FittedBox(child: Text(text: args.label)),
      onPressed: args.onPressed,
    ),
  ),
);

Subsequently, Button is defined in terms of a GestureDetector local widget (which is defined in terms of the GestureDetector widget from the Flutter framework), and the args.onPressed is passed to the onTap argument of that GestureDetector local widget (and from there subsequently to the Flutter framework widget).

When all is said and done, and the button is pressed, an event with the name "digit" and the given arguments is reported to the RemoteWidget's onEvent callback. That callback takes two arguments, the event name and the event arguments.

On the implementation side, in local widget libraries, arguments like the onTap of the GestureDetector local widget must be turned into a Dart closure that is passed to the actual Flutter widget called GestureDetector as the value of its onTap callback.

The simplest kind of callback closure is a VoidCallback (no arguments, no return value). To turn an event value in a local widget's arguments in the local widget library into a VoidCallback in Dart that reports the event as described above, the DataSource.voidHandler method is used. For example, here is a simplified GestureDetector local widget that just implements onTap (when implementing similar local widgets, you may use a similar technique):

return <WidgetLibrary>[
  LocalWidgetLibrary(<String, LocalWidgetBuilder>{
    // The local widget is called `GestureDetector`...
    'GestureDetector': (BuildContext context, DataSource source) {
      // The local widget is implemented using the `GestureDetector`
      // widget from the Flutter framework.
      return GestureDetector(
        onTap: source.voidHandler(<Object>['onTap']),
        // A full implementation of a `GestureDetector` local widget
        // would have more arguments here, like `onTapDown`, etc.
        child: source.optionalChild(<Object>['child']),
      );
    },
  }),
];

Sometimes, a callback has a different signature, in particular, it may provide arguments. To convert the event value into a Dart callback closure that reports an event as described above, the DataSource.handler method is used.

In addition to the list of keys that identify the event value, the method itself takes a callback closure. That callback's purpose is to convert the given trigger (a function which, when called, reports the event) into the kind of callback closure the Widget expects. This is usually written something like the following:

return GestureDetector(
  onTapDown: source.handler(<Object>['onTapDown'], (HandlerTrigger trigger) => (TapDownDetails details) => trigger()),
  child: source.optionalChild(<Object>['child']),
);

To break this down more clearly:

return GestureDetector(
  // onTapDown expects a function that takes a TapDownDetails
  onTapDown: source.handler<GestureTapDownCallback>( // this returns a function that takes a TapDownDetails
    <Object>['onTapDown'],
    (HandlerTrigger trigger) { // "trigger" is the function that will send the event to RemoteWidget.onEvent
      return (TapDownDetails details) { // this is the function that is returned by handler() above
        trigger(); // the function calls "trigger"
      };
    },
  ),
  child: source.optionalChild(<Object>['child']),
);

In some cases, the arguments sent to the callback (the TapDownDetails in this case) are useful and should be passed to the RemoteWidget.onEvent as part of its arguments. This can be done by passing some values to the trigger method, as in:

return GestureDetector(
  onTapDown: source.handler(<Object>['onTapDown'], (HandlerTrigger trigger) {
    return (TapDownDetails details) => trigger(<String, Object>{
      'x': details.globalPosition.dx,
      'y': details.globalPosition.dy,
    });
  }),
  child: source.optionalChild(<Object>['child']),
);

Any arguments in the event get merged with the arguments passed to the trigger.

Animations

The rfw package introduces a new Flutter widget called AnimationDefaults.

This widget is exposed by createCoreWidgets under the same name, and can be exposed in other local widget libraries as desired. This allows remote widget libraries to configure the animation speed and curves of entire subtrees more conveniently than repeating the details for each widget.

To support this widget, implement curve arguments using ArgumentDecoders.curve and duration arguments using ArgumentDecoders.duration. This automatically defers to the defaults provided by AnimationDefaults. Alternatively, the AnimationDefaults.curveOf and AnimationDefaults.durationOf methods can be used with a BuildContext directly to get curve and duration settings for animations.

The settings default to 200ms and the Curves.fastOutSlowIn curve.

Developing remote widget libraries

Remote widget libraries are usually defined using a Remote Flutter Widgets text library file (rfwtxt extension), which is then compiled into a binary library file (rfw extension) on the server before being sent to the client.

The format of text library files is defined in detail in the API documentation of the parseLibraryFile function.

Compiling a text rfwtxt file to the binary rfw format can be done by calling encodeLibraryBlob on the results of calling parseLibraryFile.

The example in example/wasm has some elaborate remote widgets, including some that manipulate state (Button).

State

The canonical example of a state-manipulating widget is a button. Buttons must react immediately (in milliseconds) and cannot wait for logic that's possibly running on a remote server (maybe many hundreds of milliseconds away).

The aforementioned Button widget in the wasm example tracks a local "down" state, manipulates it in reaction to onTapDown/onTapUp events, and changes the shadow and margins of the button based on its state:

widget Button { down: false } = GestureDetector(
  onTap: args.onPressed,
  onTapDown: set state.down = true,
  onTapUp: set state.down = false,
  onTapCancel: set state.down = false,
  child: Container(
    duration: 50,
    margin: switch state.down {
      false: [ 0.0, 0.0, 2.0, 2.0 ],
      true: [ 2.0, 2.0, 0.0, 0.0 ],
    },
    padding: [ 12.0, 8.0 ],
    decoration: {
      type: "shape",
      shape: {
        type: "stadium",
        side: { width: 1.0 },
      },
      gradient: {
        type: "linear",
        begin: { x: -0.5, y: -0.25 },
        end: { x: 0.0, y: 0.5 },
        colors: [ 0xFFFFFF99, 0xFFEEDD00 ],
        stops: [ 0.0, 1.0 ],
        tileMode: "mirror",
      },
      shadows: switch state.down {
        false: [ { blurRadius: 4.0, spreadRadius: 0.5, offset: { x: 1.0, y: 1.0, } } ],
        default: [],
      },
    },
    child: DefaultTextStyle(
      style: {
        color: 0xFF000000,
        fontSize: 32.0,
      },
      child: args.child,
    ),
  ),
);

Because Container is implemented in createCoreWidgets using the AnimatedContainer widget, changing the fields causes the button to animate. The duration: 50 argument sets the animation speed to 50ms.

Lists

Let us consider a remote widget library that is used to render data in this form:

{ "games": [
{"rating": 8.219, "users-rated": 16860, "name": "Twilight Struggle", "rank": 1, "link": "/boardgame/12333/twilight-struggle", "id": 12333},
{"rating": 8.093, "users-rated": 11750, "name": "Through the Ages: A Story of Civilization", "rank": 2, "link": "/boardgame/25613/through-ages-story-civilization", "id": 25613},
{"rating": 8.088, "users-rated": 34745, "name": "Agricola", "rank": 3, "link": "/boardgame/31260/agricola", "id": 31260},
{"rating": 8.082, "users-rated": 8913, "name": "Terra Mystica", "rank": 4, "link": "/boardgame/120677/terra-mystica", "id": 120677},
// ···

For the sake of this example, let us assume this data is registered with the DynamicContent under the name server.

This configuration data is both valid JSON and a valid RFW data file, which shows how similar the two syntaxes are.

This data is parsed by calling parseDataFile, which turns it into DynamicMap. That object is then passed to a DynamicContent, using DynamicContent.update (this is where the name server would be specified) which is passed to a RemoteWidget via the data property.

Ideally, rather than dealing with this text form on the client, the data would be turned into a binary form using encodeDataBlob on the server, and then parsed on the client using decodeDataBlob.

First, let's render a plain Flutter ListView with the name of each product. The Shop widget below achieves this:

import core;

widget Shop = ListView(
  children: [
    Text(text: "Products:"),
    ...for product in data.server.games:
      Product(product: product)
  ],
);

widget Product = Text(text: args.product.name, softWrap: false, overflow: "fade");

The Product widget here is not strictly necessary, it could be inlined into the Shop. However, as with Flutter itself, it can be easier to develop widgets when logically separate components are separated into separate widgets.

We can elaborate on this example, introducing a Material AppBar, using a ListTile for the list items, and making them interactive (at least in principle; the logic in the app would need to know how to handle the "shop.productSelect" event):

import core;
import material;

widget MaterialShop = Scaffold(
  appBar: AppBar(
    title: Text(text: ['Products']),
  ),
  body: ListView(
    children: [
      ...for product in data.server.games:
        Product(product: product)
    ],
  ),
);

widget Product = ListTile(
  title: Text(text: args.product.name),
  onTap: event 'shop.productSelect' { name: args.product.name, path: args.product.link },
);

Fetching remote widget libraries remotely

The example in example/remote shows how a program could fetch different user interfaces at runtime. In this example, the interface used on startup is the one last cached locally. Each time the program is run, after displaying the currently-cached interface, the application fetches a new interface over the network, overwriting the one in the cache, so that a different interface is used the next time the app is run.

This example also shows how an application can implement custom local code for events; in this case, incrementing a counter (both of the "remote" widgets are just different ways of implementing a counter).

Integrating with scripting language runtimes

The example in example/wasm shows how a program could fetch logic in addition to UI, in this case using Wasm compiled from C (and let us briefly appreciate the absurdity of using C as a scripting language for an application written in Dart).

In this example, as written, the Dart client could support any application whose data model consisted of a single integer and whose logic could be expressed in C without external dependencies.

This example could be extended to have the C program export data in the Remote Flutter Widgets binary data blob format which could be parsed using decodeDataBlob and passed to DynamicContent.update (thus allowing any structured data supported by RFW), and similarly arguments could be passed to the Wasm code using the same format (encoding using encodeDataBlob) to allow arbitrary structured data to be communicated from the interface to the Wasm logic. In addition, the Wasm logic could be provided with WASI interface bindings or with custom bindings that expose platform capabilities (e.g. from Flutter plugins), greatly extending the scope of what could be implemented in the Wasm logic.

As of the time of writing, package:wasm does not support Android, iOS, or web, so this demo is limited to desktop environments. The underlying Wasmer runtime supports Android and iOS already, and obviously Wasm in general is supported by web browsers, so it is expected that these limitations are only temporary (modulo policy concerns on iOS, anyway).

Contributing

See CONTRIBUTING.md

Examples

Here are some examples of rendering a UI format based on a text description.

Description: "Products screen with a list of products and listen for a onTap event for each one for the following json: {"data": "server": {"games": [{"name":"Product 1", "link": "https://product.link/product1"}]}}"

widget root = Scaffold(
  appBar: AppBar(
    title: Text(text: ['Products']),
  ),
  body: ListView(
    children: [
      ...for product in data.server.games:
        Product(product: product)
    ],
  ),
);

widget Product = ListTile(
  title: Text(text: args.product.name),
  onTap: event 'shop.productSelect' { name: args.product.name, path: args.product.link },
);

Description: "Center text 'Hello World'"

widget root = Center(
  child: Text(text: ['Hello World']),
);

Always return "widget root =".

Always include "import core;" and/or "import material;" at the top to make sure it renders correctly. Check to see where the components come from and import accordingly.

For callbacks like "onPressed" use the event format "event tag args". For example "event 'click' {}".

Colors should define a hex directly color: 0xFF00FF00 and not include Color class.

Do not include MaterialStateProperty for any attributes.

Ranges should be defined as valid JS.

For Enums return just the string of the name, for example "TextAlign.center" would be "'center'"

You are a generative UI API that only responds with valid RFW UI code. Generate the UI based on the description the user provides.

Each response should return in Markdown syntax with rfw/json/javascript tags for each type:

UI State:

{}

UI Code:

import core;
import material;

widget root = ...;

Script code:

function onEvent(e, args) {
...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment