[rfw] Add support for widget builders (#5907)

This PR adds support for widget builders.

https://github.com/flutter/flutter/issues/141658
This commit is contained in:
Juanjo Tugores
2024-03-06 13:02:06 -08:00
committed by GitHub
parent ceb3dfd15f
commit 6701c9e618
9 changed files with 1285 additions and 87 deletions

View File

@ -1,3 +1,6 @@
## 1.0.25
* Adds support for wildget builders.
## 1.0.24
* Adds `InkResponse` material widget.

View File

@ -298,6 +298,8 @@ const int _msEvent = 0x0E;
const int _msSwitch = 0x0F;
const int _msDefault = 0x10;
const int _msSetState = 0x11;
const int _msWidgetBuilder = 0x12;
const int _msWidgetBuilderArgReference = 0x13;
/// API for decoding Remote Flutter Widgets binary blobs.
///
@ -453,6 +455,10 @@ class _BlobDecoder {
return _readSwitch();
case _msSetState:
return SetStateHandler(StateReference(_readPartList()), _readArgument());
case _msWidgetBuilder:
return _readWidgetBuilder();
case _msWidgetBuilderArgReference:
return WidgetBuilderArgReference(_readString(), _readPartList());
default:
return _parseValue(type, _readArgument);
}
@ -468,6 +474,16 @@ class _BlobDecoder {
return ConstructorCall(name, _readMap(_readArgument)!);
}
WidgetBuilderDeclaration _readWidgetBuilder() {
final String argumentName = _readString();
final int type = _readByte();
if (type != _msWidget && type != _msSwitch) {
throw FormatException('Unrecognized data type 0x${type.toRadixString(16).toUpperCase().padLeft(2, "0")} while decoding widget builder blob.');
}
final BlobNode widget = type == _msWidget ? _readWidget() : _readSwitch();
return WidgetBuilderDeclaration(argumentName, widget);
}
WidgetDeclaration _readDeclaration() {
final String name = _readString();
final DynamicMap? initialState = _readMap(readValue, nullIfEmpty: true);
@ -613,6 +629,10 @@ class _BlobEncoder {
bytes.addByte(_msWidget);
_writeString(value.name);
_writeMap(value.arguments, _writeArgument);
} else if (value is WidgetBuilderDeclaration) {
bytes.addByte(_msWidgetBuilder);
_writeString(value.argumentName);
_writeArgument(value.widget);
} else if (value is ArgsReference) {
bytes.addByte(_msArgsReference);
_writeInt64(value.parts.length);
@ -621,6 +641,11 @@ class _BlobEncoder {
bytes.addByte(_msDataReference);
_writeInt64(value.parts.length);
value.parts.forEach(_writePart);
} else if (value is WidgetBuilderArgReference) {
bytes.addByte(_msWidgetBuilderArgReference);
_writeString(value.argumentName);
_writeInt64(value.parts.length);
value.parts.forEach(_writePart);
} else if (value is LoopReference) {
bytes.addByte(_msLoopReference);
_writeInt64(value.loop);

View File

@ -439,6 +439,28 @@ class ConstructorCall extends BlobNode {
String toString() => '$name($arguments)';
}
/// Representation of functions that return widgets in Remote Flutter Widgets library blobs.
class WidgetBuilderDeclaration extends BlobNode {
/// Represents a callback that takes a single argument [argumentName] and returns the [widget].
const WidgetBuilderDeclaration(this.argumentName, this.widget);
/// The callback single argument name.
///
/// In `Builder(builder: (scope) => Container());`, [argumentName] is "scope".
final String argumentName;
/// The widget that will be returned when the builder callback is called.
///
/// This is usually a [ConstructorCall], but may be a [Switch] (so long as
/// that [Switch] resolves to a [ConstructorCall]. Other values (or a [Switch]
/// that does not resolve to a constructor call) will result in an
/// [ErrorWidget] being used.
final BlobNode widget;
@override
String toString() => '($argumentName) => $widget';
}
/// Base class for various kinds of references in the RFW data structures.
abstract class Reference extends BlobNode {
/// Abstract const constructor. This constructor enables subclasses to provide
@ -534,6 +556,31 @@ class DataReference extends Reference {
String toString() => 'data.${parts.join(".")}';
}
/// Reference to the single argument of type [DynamicMap] passed into the widget builder.
///
/// This class is used to represent references to a function argument.
/// In `(scope) => Container(width: scope.width)`, this represents "scope.width".
///
/// See also:
///
/// * [WidgetBuilderDeclaration], which represents a widget builder definition.
class WidgetBuilderArgReference extends Reference {
/// Wraps the given [argumentName] and [parts] as a [WidgetBuilderArgReference].
///
/// The parts must not be mutated after the object is created.
const WidgetBuilderArgReference(this.argumentName, super.parts);
/// A reference to a [WidgetBuilderDeclaration.argumentName].
///
/// In `Builder(builder: (scope) => Text(text: scope.result.text));`,
/// "scope.result.text" is the [WidgetBuilderArgReference].
/// The [argumentName] is "scope" and its [parts] are `["result", "text"]`.
final String argumentName;
@override
String toString() => '$argumentName.${parts.join('.')}';
}
/// Unbound reference to a [Loop].
class LoopReference extends Reference {
/// Wraps the given [loop] and [parts] as a [LoopReference].

View File

@ -272,8 +272,8 @@ DynamicMap parseDataFile(String file) {
/// declaration, along with its arguments. Arguments are a map of key-value
/// pairs, where the values can be any of the types in the data model defined
/// above plus any of the types defined below in this section, such as
/// references to arguments, the data model, loops, state, switches, or
/// event handlers.
/// references to arguments, the data model, widget builders, loops, state,
/// switches or event handlers.
///
/// In this example, several constructor calls are nested together:
///
@ -283,6 +283,9 @@ DynamicMap parseDataFile(String file) {
/// Container(
/// child: Text(text: "Hello"),
/// ),
/// Builder(
/// builder: (scope) => Text(text: scope.world),
/// ),
/// ],
/// );
/// ```
@ -293,6 +296,35 @@ DynamicMap parseDataFile(String file) {
/// constructor call also has only one argument, `child`, whose value, again, is
/// a constructor call, in this case creating a `Text` widget.
///
/// ### Widget Builders
///
/// Widget builders take a single argument and return a widget.
/// The [DynamicMap] argument consists of key-value pairs where values
/// can be of any types in the data model. Widget builders arguments are lexically
/// scoped so a given constructor call has access to any arguments where it is
/// defined plus arguments defined by its parents (if any).
///
/// In this example several widget builders are nested together:
///
/// ```
/// widget Foo {text: 'this is cool'} = Builder(
/// builder: (foo) => Builder(
/// builder: (bar) => Builder(
/// builder: (baz) => Text(
/// text: [
/// args.text,
/// state.text,
/// data.text,
/// foo.text,
/// bar.text,
/// baz.text,
/// ],
/// ),
/// ),
/// ),
/// );
/// ```
///
/// ### References
///
/// Remote widget libraries typically contain _references_, e.g. to the
@ -610,6 +642,12 @@ const Set<String> _reservedWords = <String>{
'true',
};
void _checkIsNotReservedWord(String identifier, _Token identifierToken) {
if (_reservedWords.contains(identifier)) {
throw ParserException._fromToken('$identifier is a reserved word', identifierToken);
}
}
sealed class _Token {
_Token(this.line, this.column, this.start, this.end);
final int line;
@ -630,6 +668,7 @@ class _SymbolToken extends _Token {
static const int colon = 0x3A; // U+003A COLON character (:)
static const int semicolon = 0x3B; // U+003B SEMICOLON character (;)
static const int equals = 0x3D; // U+003D EQUALS SIGN character (=)
static const int greatherThan = 0x3E; // U+003D GREATHER THAN character (>)
static const int openBracket = 0x5B; // U+005B LEFT SQUARE BRACKET character ([)
static const int closeBracket = 0x5D; // U+005D RIGHT SQUARE BRACKET character (])
static const int openBrace = 0x7B; // U+007B LEFT CURLY BRACKET character ({)
@ -812,6 +851,7 @@ Iterable<_Token> _tokenize(String file) sync* {
case 0x3A: // U+003A COLON character (:)
case 0x3B: // U+003B SEMICOLON character (;)
case 0x3D: // U+003D EQUALS SIGN character (=)
case 0x3E: // U+003E GREATHER THAN SIGN character (>)
case 0x5B: // U+005B LEFT SQUARE BRACKET character ([)
case 0x5D: // U+005D RIGHT SQUARE BRACKET character (])
case 0x7B: // U+007B LEFT CURLY BRACKET character ({)
@ -2132,14 +2172,23 @@ class _Parser {
return _readString();
}
DynamicMap _readMap({ required bool extended }) {
DynamicMap _readMap({
required bool extended,
List<String> widgetBuilderScope = const <String>[],
}) {
_expectSymbol(_SymbolToken.openBrace);
final DynamicMap results = _readMapBody(extended: extended);
final DynamicMap results = _readMapBody(
widgetBuilderScope: widgetBuilderScope,
extended: extended,
);
_expectSymbol(_SymbolToken.closeBrace);
return results;
}
DynamicMap _readMapBody({ required bool extended }) {
DynamicMap _readMapBody({
required bool extended,
List<String> widgetBuilderScope = const <String>[],
}) {
final DynamicMap results = DynamicMap(); // ignore: prefer_collection_literals
while (_source.current is! _SymbolToken) {
final String key = _readKey();
@ -2147,7 +2196,11 @@ class _Parser {
throw ParserException._fromToken('Duplicate key "$key" in map', _source.current);
}
_expectSymbol(_SymbolToken.colon);
final Object value = _readValue(extended: extended, nullOk: true);
final Object value = _readValue(
extended: extended,
nullOk: true,
widgetBuilderScope: widgetBuilderScope,
);
if (value != missing) {
results[key] = value;
}
@ -2162,7 +2215,10 @@ class _Parser {
final List<String> _loopIdentifiers = <String>[];
DynamicList _readList({ required bool extended }) {
DynamicList _readList({
required bool extended,
List<String> widgetBuilderScope = const <String>[],
}) {
final DynamicList results = DynamicList.empty(growable: true);
_expectSymbol(_SymbolToken.openBracket);
while (!_foundSymbol(_SymbolToken.closeBracket)) {
@ -2172,19 +2228,26 @@ class _Parser {
_expectIdentifier('for');
final _Token loopIdentifierToken = _source.current;
final String loopIdentifier = _readIdentifier();
if (_reservedWords.contains(loopIdentifier)) {
throw ParserException._fromToken('$loopIdentifier is a reserved word', loopIdentifierToken);
}
_checkIsNotReservedWord(loopIdentifier, loopIdentifierToken);
_expectIdentifier('in');
final Object collection = _readValue(extended: true);
final Object collection = _readValue(
widgetBuilderScope: widgetBuilderScope,
extended: true,
);
_expectSymbol(_SymbolToken.colon);
_loopIdentifiers.add(loopIdentifier);
final Object template = _readValue(extended: extended);
final Object template = _readValue(
widgetBuilderScope: widgetBuilderScope,
extended: extended,
);
assert(_loopIdentifiers.last == loopIdentifier);
_loopIdentifiers.removeLast();
results.add(_withSourceRange(Loop(collection, template), start));
} else {
final Object value = _readValue(extended: extended);
final Object value = _readValue(
widgetBuilderScope: widgetBuilderScope,
extended: extended,
);
results.add(value);
}
if (_foundSymbol(_SymbolToken.comma)) {
@ -2197,8 +2260,10 @@ class _Parser {
return results;
}
Switch _readSwitch(SourceLocation? start) {
final Object value = _readValue(extended: true);
Switch _readSwitch(SourceLocation? start, {
List<String> widgetBuilderScope = const <String>[],
}) {
final Object value = _readValue(extended: true, widgetBuilderScope: widgetBuilderScope);
final Map<Object?, Object> cases = <Object?, Object>{};
_expectSymbol(_SymbolToken.openBrace);
while (_source.current is! _SymbolToken) {
@ -2210,13 +2275,13 @@ class _Parser {
key = null;
_advance();
} else {
key = _readValue(extended: true);
key = _readValue(extended: true, widgetBuilderScope: widgetBuilderScope);
if (cases.containsKey(key)) {
throw ParserException._fromToken('Switch has duplicate cases for key $key', _source.current);
}
}
_expectSymbol(_SymbolToken.colon);
final Object value = _readValue(extended: true);
final Object value = _readValue(extended: true, widgetBuilderScope: widgetBuilderScope);
cases[key] = value;
if (_foundSymbol(_SymbolToken.comma)) {
_advance();
@ -2249,13 +2314,19 @@ class _Parser {
return results;
}
Object _readValue({ required bool extended, bool nullOk = false }) {
Object _readValue({
required bool extended,
bool nullOk = false,
List<String> widgetBuilderScope = const <String>[],
}) {
if (_source.current is _SymbolToken) {
switch ((_source.current as _SymbolToken).symbol) {
case _SymbolToken.openBracket:
return _readList(extended: extended);
return _readList(widgetBuilderScope: widgetBuilderScope, extended: extended);
case _SymbolToken.openBrace:
return _readMap(extended: extended);
return _readMap(widgetBuilderScope: widgetBuilderScope, extended: extended);
case _SymbolToken.openParen:
return _readWidgetBuilderDeclaration(widgetBuilderScope: widgetBuilderScope);
}
} else if (_source.current is _IntegerToken) {
final Object result = (_source.current as _IntegerToken).value;
@ -2289,7 +2360,13 @@ class _Parser {
if (identifier == 'event') {
final SourceLocation? start = _getSourceLocation();
_advance();
return _withSourceRange(EventHandler(_readString(), _readMap(extended: true)), start);
return _withSourceRange(
EventHandler(
_readString(),
_readMap(widgetBuilderScope: widgetBuilderScope, extended: true),
),
start,
);
}
if (identifier == 'args') {
final SourceLocation? start = _getSourceLocation();
@ -2309,7 +2386,7 @@ class _Parser {
if (identifier == 'switch') {
final SourceLocation? start = _getSourceLocation();
_advance();
return _readSwitch(start);
return _readSwitch(start, widgetBuilderScope: widgetBuilderScope);
}
if (identifier == 'set') {
final SourceLocation? start = _getSourceLocation();
@ -2318,25 +2395,56 @@ class _Parser {
_expectIdentifier('state');
final StateReference stateReference = _withSourceRange(StateReference(_readParts()), innerStart);
_expectSymbol(_SymbolToken.equals);
final Object value = _readValue(extended: true);
final Object value = _readValue(widgetBuilderScope: widgetBuilderScope, extended: true);
return _withSourceRange(SetStateHandler(stateReference, value), start);
}
if (widgetBuilderScope.contains(identifier)) {
final SourceLocation? start = _getSourceLocation();
_advance();
return _withSourceRange(WidgetBuilderArgReference(identifier, _readParts()), start);
}
final int index = _loopIdentifiers.lastIndexOf(identifier) + 1;
if (index > 0) {
final SourceLocation? start = _getSourceLocation();
_advance();
return _withSourceRange(LoopReference(_loopIdentifiers.length - index, _readParts(optional: true)), start);
}
return _readConstructorCall();
return _readConstructorCall(widgetBuilderScope: widgetBuilderScope);
}
throw ParserException._unexpected(_source.current);
}
ConstructorCall _readConstructorCall() {
WidgetBuilderDeclaration _readWidgetBuilderDeclaration({
List<String> widgetBuilderScope = const <String>[],
}) {
_expectSymbol(_SymbolToken.openParen);
final _Token argumentNameToken = _source.current;
final String argumentName = _readIdentifier();
_checkIsNotReservedWord(argumentName, argumentNameToken);
_expectSymbol(_SymbolToken.closeParen);
_expectSymbol(_SymbolToken.equals);
_expectSymbol(_SymbolToken.greatherThan);
final _Token valueToken = _source.current;
final Object widget = _readValue(
extended: true,
widgetBuilderScope: <String>[...widgetBuilderScope, argumentName],
);
if (widget is! ConstructorCall && widget is! Switch) {
throw ParserException._fromToken('Expecting a switch or constructor call got $widget', valueToken);
}
return WidgetBuilderDeclaration(argumentName, widget as BlobNode);
}
ConstructorCall _readConstructorCall({
List<String> widgetBuilderScope = const <String>[],
}) {
final SourceLocation? start = _getSourceLocation();
final String name = _readIdentifier();
_expectSymbol(_SymbolToken.openParen);
final DynamicMap arguments = _readMapBody(extended: true);
final DynamicMap arguments = _readMapBody(
extended: true,
widgetBuilderScope: widgetBuilderScope,
);
_expectSymbol(_SymbolToken.closeParen);
return _withSourceRange(ConstructorCall(name, arguments), start);
}

View File

@ -19,6 +19,9 @@ import 'content.dart';
/// [LocalWidgetBuilder] callbacks.
typedef LocalWidgetBuilder = Widget Function(BuildContext context, DataSource source);
/// Signature of builders for remote widgets.
typedef _RemoteWidgetBuilder = _CurriedWidget Function(DynamicMap builderArg);
/// Signature of the callback passed to a [RemoteWidget].
///
/// This is used by [RemoteWidget] and [Runtime.build] as the callback for
@ -126,6 +129,25 @@ abstract class DataSource {
/// non-widget nodes replaced by [ErrorWidget].
List<Widget> childList(List<Object> argsKey);
/// Builds the widget builder at the given key.
///
/// If the node is not a widget builder, returns an [ErrorWidget].
///
/// See also:
///
/// * [optionalBuilder], which returns null if the widget builder is missing.
Widget builder(List<Object> argsKey, DynamicMap builderArg);
/// Builds the widget builder at the given key.
///
/// If the node is not a widget builder, returns null.
///
/// See also:
///
/// * [builder], which returns an [ErrorWidget] instead of null if the widget
/// builder is missing.
Widget? optionalBuilder(List<Object> argsKey, DynamicMap builderArg);
/// Gets a [VoidCallback] event handler at the given key.
///
/// If the node specified is an [AnyEventHandler] or a [DynamicList] of
@ -284,11 +306,23 @@ class Runtime extends ChangeNotifier {
///
/// The `remoteEventTarget` argument is the callback that the RFW runtime will
/// invoke whenever a remote widget event handler is triggered.
Widget build(BuildContext context, FullyQualifiedWidgetName widget, DynamicContent data, RemoteEventHandler remoteEventTarget) {
Widget build(
BuildContext context,
FullyQualifiedWidgetName widget,
DynamicContent data,
RemoteEventHandler remoteEventTarget,
) {
_CurriedWidget? boundWidget = _widgets[widget];
if (boundWidget == null) {
_checkForImportLoops(widget.library);
boundWidget = _applyConstructorAndBindArguments(widget, const <String, Object?>{}, -1, <FullyQualifiedWidgetName>{}, null);
boundWidget = _applyConstructorAndBindArguments(
widget,
const <String, Object?>{},
const <String, Object?>{},
-1,
<FullyQualifiedWidgetName>{},
null,
);
_widgets[widget] = boundWidget;
}
return boundWidget.build(context, data, remoteEventTarget, const <_WidgetState>[]);
@ -410,13 +444,22 @@ class Runtime extends ChangeNotifier {
/// [LocalWidgetBuilder] rather than a [WidgetDeclaration], and is used to
/// provide source information for local widgets (which otherwise could not be
/// associated with a part of the source). See also [Runtime.blobNodeFor].
_CurriedWidget _applyConstructorAndBindArguments(FullyQualifiedWidgetName fullName, DynamicMap arguments, int stateDepth, Set<FullyQualifiedWidgetName> usedWidgets, BlobNode? source) {
_CurriedWidget _applyConstructorAndBindArguments(
FullyQualifiedWidgetName fullName,
DynamicMap arguments,
DynamicMap widgetBuilderScope,
int stateDepth,
Set<FullyQualifiedWidgetName> usedWidgets,
BlobNode? source,
) {
final _ResolvedConstructor? widget = _findConstructor(fullName);
if (widget != null) {
if (widget.constructor is WidgetDeclaration) {
if (usedWidgets.contains(widget.fullName)) {
return _CurriedLocalWidget.error(fullName, 'Widget loop: Tried to call ${widget.fullName} constructor reentrantly.')
..propagateSource(source);
return _CurriedLocalWidget.error(
fullName,
'Widget loop: Tried to call ${widget.fullName} constructor reentrantly.',
)..propagateSource(source);
}
usedWidgets = usedWidgets.toSet()..add(widget.fullName);
final WidgetDeclaration constructor = widget.constructor as WidgetDeclaration;
@ -426,22 +469,43 @@ class Runtime extends ChangeNotifier {
} else {
newDepth = stateDepth;
}
Object result = _bindArguments(widget.fullName, constructor.root, arguments, newDepth, usedWidgets);
Object result = _bindArguments(
widget.fullName,
constructor.root,
arguments,
widgetBuilderScope,
newDepth,
usedWidgets,
);
if (result is Switch) {
result = _CurriedSwitch(widget.fullName, result, arguments, constructor.initialState)
..propagateSource(result);
result = _CurriedSwitch(
widget.fullName,
result,
arguments,
widgetBuilderScope,
constructor.initialState,
)..propagateSource(result);
} else {
result as _CurriedWidget;
if (constructor.initialState != null) {
result = _CurriedRemoteWidget(widget.fullName, result, arguments, constructor.initialState)
..propagateSource(result);
result = _CurriedRemoteWidget(
widget.fullName,
result,
arguments,
widgetBuilderScope,
constructor.initialState,
)..propagateSource(result);
}
}
return result as _CurriedWidget;
}
assert(widget.constructor is LocalWidgetBuilder);
return _CurriedLocalWidget(widget.fullName, widget.constructor as LocalWidgetBuilder, arguments)
..propagateSource(source);
return _CurriedLocalWidget(
widget.fullName,
widget.constructor as LocalWidgetBuilder,
arguments,
widgetBuilderScope,
)..propagateSource(source);
}
final Set<LibraryName> missingLibraries = _findMissingLibraries(fullName.library).toSet();
if (missingLibraries.isNotEmpty) {
@ -455,37 +519,93 @@ class Runtime extends ChangeNotifier {
..propagateSource(source);
}
Object _bindArguments(FullyQualifiedWidgetName context, Object node, Object arguments, int stateDepth, Set<FullyQualifiedWidgetName> usedWidgets) {
Object _bindArguments(
FullyQualifiedWidgetName context,
Object node, Object arguments,
DynamicMap widgetBuilderScope,
int stateDepth,
Set<FullyQualifiedWidgetName> usedWidgets,
) {
if (node is ConstructorCall) {
final DynamicMap subArguments = _bindArguments(context, node.arguments, arguments, stateDepth, usedWidgets) as DynamicMap;
return _applyConstructorAndBindArguments(FullyQualifiedWidgetName(context.library, node.name), subArguments, stateDepth, usedWidgets, node);
final DynamicMap subArguments = _bindArguments(
context,
node.arguments,
arguments,
widgetBuilderScope,
stateDepth,
usedWidgets,
) as DynamicMap;
return _applyConstructorAndBindArguments(
FullyQualifiedWidgetName(context.library, node.name),
subArguments,
widgetBuilderScope,
stateDepth,
usedWidgets,
node,
);
}
if (node is WidgetBuilderDeclaration) {
return (DynamicMap widgetBuilderArg) {
final DynamicMap newWidgetBuilderScope = <String, Object?> {
...widgetBuilderScope,
node.argumentName: widgetBuilderArg,
};
final Object result = _bindArguments(
context,
node.widget,
arguments,
newWidgetBuilderScope,
stateDepth,
usedWidgets,
);
if (result is Switch) {
return _CurriedSwitch(
FullyQualifiedWidgetName(context.library, ''),
result,
arguments as DynamicMap,
newWidgetBuilderScope,
const <String, Object?>{},
)..propagateSource(result);
}
return result as _CurriedWidget;
};
}
if (node is DynamicMap) {
return node.map<String, Object?>(
(String name, Object? value) => MapEntry<String, Object?>(name, _bindArguments(context, value!, arguments, stateDepth, usedWidgets)),
(String name, Object? value) => MapEntry<String, Object?>(
name,
_bindArguments(context, value!, arguments, widgetBuilderScope, stateDepth, usedWidgets),
),
);
}
if (node is DynamicList) {
return List<Object>.generate(
node.length,
(int index) => _bindArguments(context, node[index]!, arguments, stateDepth, usedWidgets),
(int index) => _bindArguments(
context,
node[index]!,
arguments,
widgetBuilderScope,
stateDepth,
usedWidgets,
),
growable: false,
);
}
if (node is Loop) {
final Object input = _bindArguments(context, node.input, arguments, stateDepth, usedWidgets);
final Object output = _bindArguments(context, node.output, arguments, stateDepth, usedWidgets);
final Object input = _bindArguments(context, node.input, arguments, widgetBuilderScope, stateDepth, usedWidgets);
final Object output = _bindArguments(context, node.output, arguments, widgetBuilderScope, stateDepth, usedWidgets);
return Loop(input, output)
..propagateSource(node);
}
if (node is Switch) {
return Switch(
_bindArguments(context, node.input, arguments, stateDepth, usedWidgets),
_bindArguments(context, node.input, arguments, widgetBuilderScope, stateDepth, usedWidgets),
node.outputs.map<Object?, Object>(
(Object? key, Object value) {
return MapEntry<Object?, Object>(
key == null ? key : _bindArguments(context, key, arguments, stateDepth, usedWidgets),
_bindArguments(context, value, arguments, stateDepth, usedWidgets),
key == null ? key : _bindArguments(context, key, arguments, widgetBuilderScope, stateDepth, usedWidgets),
_bindArguments(context, value, arguments, widgetBuilderScope, stateDepth, usedWidgets),
);
},
),
@ -498,14 +618,25 @@ class Runtime extends ChangeNotifier {
return node.bind(stateDepth)..propagateSource(node);
}
if (node is EventHandler) {
return EventHandler(node.eventName, _bindArguments(context, node.eventArguments, arguments, stateDepth, usedWidgets) as DynamicMap)
..propagateSource(node);
return EventHandler(
node.eventName,
_bindArguments(
context,
node.eventArguments,
arguments,
widgetBuilderScope,
stateDepth,
usedWidgets,
) as DynamicMap,
)..propagateSource(node);
}
if (node is SetStateHandler) {
assert(node.stateReference is StateReference);
final BoundStateReference stateReference = (node.stateReference as StateReference).bind(stateDepth);
return SetStateHandler(stateReference, _bindArguments(context, node.value, arguments, stateDepth, usedWidgets))
..propagateSource(node);
return SetStateHandler(
stateReference,
_bindArguments(context, node.value, arguments, widgetBuilderScope, stateDepth, usedWidgets),
)..propagateSource(node);
}
assert(node is! WidgetDeclaration);
return node;
@ -528,12 +659,19 @@ class _ResolvedDynamicList {
typedef _DataResolverCallback = Object Function(List<Object> dataKey);
typedef _StateResolverCallback = Object Function(List<Object> stateKey, int depth);
typedef _WidgetBuilderArgResolverCallback = Object Function(List<Object> argKey);
abstract class _CurriedWidget extends BlobNode {
const _CurriedWidget(this.fullName, this.arguments, this.initialState);
const _CurriedWidget(
this.fullName,
this.arguments,
this.widgetBuilderScope,
this.initialState,
);
final FullyQualifiedWidgetName fullName;
final DynamicMap arguments;
final DynamicMap widgetBuilderScope;
final DynamicMap? initialState;
static Object _bindLoopVariable(Object node, Object argument, int depth) {
@ -569,6 +707,7 @@ abstract class _CurriedWidget extends BlobNode {
node.fullName,
node.child,
_bindLoopVariable(node.arguments, argument, depth) as DynamicMap,
_bindLoopVariable(node.widgetBuilderScope, argument, depth) as DynamicMap,
)..propagateSource(node);
}
if (node is _CurriedRemoteWidget) {
@ -576,6 +715,7 @@ abstract class _CurriedWidget extends BlobNode {
node.fullName,
_bindLoopVariable(node.child, argument, depth) as _CurriedWidget,
_bindLoopVariable(node.arguments, argument, depth) as DynamicMap,
_bindLoopVariable(node.widgetBuilderScope, argument, depth) as DynamicMap,
node.initialState,
)..propagateSource(node);
}
@ -584,6 +724,7 @@ abstract class _CurriedWidget extends BlobNode {
node.fullName,
_bindLoopVariable(node.root, argument, depth) as Switch,
_bindLoopVariable(node.arguments, argument, depth) as DynamicMap,
_bindLoopVariable(node.widgetBuilderScope, argument, depth) as DynamicMap,
node.initialState,
)..propagateSource(node);
}
@ -615,7 +756,13 @@ abstract class _CurriedWidget extends BlobNode {
//
// TODO(ianh): This really should have some sort of caching. Right now, evaluating a whole list
// ends up being around O(N^2) since we have to walk the list from the start for every entry.
static _ResolvedDynamicList _listLookup(DynamicList list, int targetEffectiveIndex, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
static _ResolvedDynamicList _listLookup(
DynamicList list,
int targetEffectiveIndex,
_StateResolverCallback stateResolver,
_DataResolverCallback dataResolver,
_WidgetBuilderArgResolverCallback widgetBuilderArgResolver,
) {
int currentIndex = 0; // where we are in `list` (some entries of which might represent multiple values, because they are themselves loops)
int effectiveIndex = 0; // where we are in the fully expanded list (the coordinate space in which we're aiming for `targetEffectiveIndex`)
while ((effectiveIndex <= targetEffectiveIndex || targetEffectiveIndex < 0) && currentIndex < list.length) {
@ -624,22 +771,46 @@ abstract class _CurriedWidget extends BlobNode {
Object inputList = node.input;
while (inputList is! DynamicList) {
if (inputList is BoundArgsReference) {
inputList = _resolveFrom(inputList.arguments, inputList.parts, stateResolver, dataResolver);
inputList = _resolveFrom(
inputList.arguments,
inputList.parts,
stateResolver,
dataResolver,
widgetBuilderArgResolver,
);
} else if (inputList is DataReference) {
inputList = dataResolver(inputList.parts);
} else if (inputList is BoundStateReference) {
inputList = stateResolver(inputList.parts, inputList.depth);
} else if (inputList is BoundLoopReference) {
inputList = _resolveFrom(inputList.value, inputList.parts, stateResolver, dataResolver);
inputList = _resolveFrom(
inputList.value,
inputList.parts,
stateResolver,
dataResolver,
widgetBuilderArgResolver,
);
} else if (inputList is Switch) {
inputList = _resolveFrom(inputList, const <Object>[], stateResolver, dataResolver);
inputList = _resolveFrom(
inputList,
const <Object>[],
stateResolver,
dataResolver,
widgetBuilderArgResolver,
);
} else {
// e.g. it's a map or something else that isn't indexable
inputList = DynamicList.empty();
}
assert(inputList is! _ResolvedDynamicList);
}
final _ResolvedDynamicList entry = _listLookup(inputList, targetEffectiveIndex >= 0 ? targetEffectiveIndex - effectiveIndex : -1, stateResolver, dataResolver);
final _ResolvedDynamicList entry = _listLookup(
inputList,
targetEffectiveIndex >= 0 ? targetEffectiveIndex - effectiveIndex : -1,
stateResolver,
dataResolver,
widgetBuilderArgResolver,
);
if (entry.result != null) {
final Object boundResult = _bindLoopVariable(node.output, entry.result!, 0);
return _ResolvedDynamicList(null, boundResult, null);
@ -656,7 +827,13 @@ abstract class _CurriedWidget extends BlobNode {
return _ResolvedDynamicList(list, null, effectiveIndex);
}
static Object _resolveFrom(Object root, List<Object> parts, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
static Object _resolveFrom(
Object root,
List<Object> parts,
_StateResolverCallback stateResolver,
_DataResolverCallback dataResolver,
_WidgetBuilderArgResolverCallback widgetBuilderArgResolver,
) {
int index = 0;
Object current = root;
while (true) {
@ -667,6 +844,9 @@ abstract class _CurriedWidget extends BlobNode {
}
current = dataResolver(current.parts);
continue;
} else if (current is WidgetBuilderArgReference) {
current = widgetBuilderArgResolver(<Object>[current.argumentName, ...current.parts]);
continue;
} else if (current is BoundArgsReference) {
List<Object> nextParts = current.parts;
if (index < parts.length) {
@ -693,7 +873,13 @@ abstract class _CurriedWidget extends BlobNode {
index = 0;
continue;
} else if (current is Switch) {
final Object key = _resolveFrom(current.input, const <Object>[], stateResolver, dataResolver);
final Object key = _resolveFrom(
current.input,
const <Object>[],
stateResolver,
dataResolver,
widgetBuilderArgResolver,
);
Object? value = current.outputs[key];
if (value == null) {
value = current.outputs[null];
@ -707,9 +893,15 @@ abstract class _CurriedWidget extends BlobNode {
// We've reached the end of the line.
// We handle some special leaf cases that still need processing before we return.
if (current is EventHandler) {
current = EventHandler(current.eventName, _fix(current.eventArguments, stateResolver, dataResolver) as DynamicMap);
current = EventHandler(
current.eventName,
_fix(current.eventArguments, stateResolver, dataResolver, widgetBuilderArgResolver) as DynamicMap,
);
} else if (current is SetStateHandler) {
current = SetStateHandler(current.stateReference, _fix(current.value, stateResolver, dataResolver));
current = SetStateHandler(
current.stateReference,
_fix(current.value, stateResolver, dataResolver, widgetBuilderArgResolver),
);
}
// else `current` is nothing special, and we'll just return it below.
break; // This is where the loop ends.
@ -725,7 +917,13 @@ abstract class _CurriedWidget extends BlobNode {
if (parts[index] is! int) {
return missing;
}
current = _listLookup(current, parts[index] as int, stateResolver, dataResolver).result ?? missing;
current = _listLookup(
current,
parts[index] as int,
stateResolver,
dataResolver,
widgetBuilderArgResolver,
).result ?? missing;
} else {
assert(current is! ArgsReference);
assert(current is! StateReference);
@ -740,27 +938,60 @@ abstract class _CurriedWidget extends BlobNode {
return current;
}
static Object _fix(Object root, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
static Object _fix(
Object root,
_StateResolverCallback stateResolver,
_DataResolverCallback dataResolver,
_WidgetBuilderArgResolverCallback widgetBuilderArgResolver,
) {
if (root is DynamicMap) {
return root.map((String key, Object? value) => MapEntry<String, Object?>(key, _fix(root[key]!, stateResolver, dataResolver)));
return root.map((String key, Object? value) =>
MapEntry<String, Object?>(
key,
_fix(root[key]!, stateResolver, dataResolver, widgetBuilderArgResolver),
),
);
} else if (root is DynamicList) {
if (root.any((Object? entry) => entry is Loop)) {
final int length = _listLookup(root, -1, stateResolver, dataResolver).length!;
return DynamicList.generate(length, (int index) => _fix(_listLookup(root, index, stateResolver, dataResolver).result!, stateResolver, dataResolver));
final int length = _listLookup(
root,
-1,
stateResolver,
dataResolver,
widgetBuilderArgResolver,
).length!;
return DynamicList.generate(
length,
(int index) => _fix(
_listLookup(root, index, stateResolver, dataResolver, widgetBuilderArgResolver).result!,
stateResolver,
dataResolver,
widgetBuilderArgResolver,
),
);
} else {
return DynamicList.generate(root.length, (int index) => _fix(root[index]!, stateResolver, dataResolver));
return DynamicList.generate(
root.length,
(int index) => _fix(root[index]!, stateResolver, dataResolver, widgetBuilderArgResolver),
);
}
} else if (root is BlobNode) {
return _resolveFrom(root, const <Object>[], stateResolver, dataResolver);
return _resolveFrom(root, const <Object>[], stateResolver, dataResolver, widgetBuilderArgResolver);
} else {
return root;
}
}
Object resolve(List<Object> parts, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver, { required bool expandLists }) {
Object result = _resolveFrom(arguments, parts, stateResolver, dataResolver);
Object resolve(
List<Object> parts,
_StateResolverCallback stateResolver,
_DataResolverCallback dataResolver,
_WidgetBuilderArgResolverCallback widgetBuilderArgResolver, {
required bool expandLists,
}) {
Object result = _resolveFrom(arguments, parts, stateResolver, dataResolver, widgetBuilderArgResolver);
if (result is DynamicList && expandLists) {
result = _listLookup(result, -1, stateResolver, dataResolver);
result = _listLookup(result, -1, stateResolver, dataResolver, widgetBuilderArgResolver);
}
assert(result is! Reference);
assert(result is! Switch);
@ -768,38 +999,92 @@ abstract class _CurriedWidget extends BlobNode {
return result;
}
Widget build(BuildContext context, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states) {
return _Widget(curriedWidget: this, data: data, remoteEventTarget: remoteEventTarget, states: states);
Widget build(
BuildContext context,
DynamicContent data,
RemoteEventHandler remoteEventTarget,
List<_WidgetState> states,
) {
return _Widget(
curriedWidget: this,
data: data,
widgetBuilderScope: DynamicContent(widgetBuilderScope),
remoteEventTarget: remoteEventTarget,
states: states,
);
}
Widget buildChild(BuildContext context, DataSource source, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver);
Widget buildChild(
BuildContext context,
DataSource source,
DynamicContent data,
RemoteEventHandler remoteEventTarget,
List<_WidgetState> states,
_StateResolverCallback stateResolver,
_DataResolverCallback dataResolver,
_WidgetBuilderArgResolverCallback widgetBuilderArgResolver,
);
@override
String toString() => '$fullName ${initialState ?? "{}"} $arguments';
}
class _CurriedLocalWidget extends _CurriedWidget {
const _CurriedLocalWidget(FullyQualifiedWidgetName fullName, this.child, DynamicMap arguments) : super(fullName, arguments, null);
const _CurriedLocalWidget(
FullyQualifiedWidgetName fullName,
this.child,
DynamicMap arguments,
DynamicMap widgetBuilderScope,
) : super(fullName, arguments, widgetBuilderScope, null);
factory _CurriedLocalWidget.error(FullyQualifiedWidgetName fullName, String message) {
return _CurriedLocalWidget(fullName, (BuildContext context, DataSource data) => _buildErrorWidget(message), const <String, Object?>{});
return _CurriedLocalWidget(
fullName,
(BuildContext context, DataSource data) => _buildErrorWidget(message),
const <String, Object?>{},
const <String, Object?>{},
);
}
final LocalWidgetBuilder child;
@override
Widget buildChild(BuildContext context, DataSource source, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
Widget buildChild(
BuildContext context,
DataSource source,
DynamicContent data,
RemoteEventHandler remoteEventTarget,
List<_WidgetState> states,
_StateResolverCallback stateResolver,
_DataResolverCallback dataResolver,
_WidgetBuilderArgResolverCallback widgetBuilderArgResolver,
) {
return child(context, source);
}
}
class _CurriedRemoteWidget extends _CurriedWidget {
const _CurriedRemoteWidget(FullyQualifiedWidgetName fullName, this.child, DynamicMap arguments, DynamicMap? initialState) : super(fullName, arguments, initialState);
const _CurriedRemoteWidget(
FullyQualifiedWidgetName fullName,
this.child,
DynamicMap arguments,
DynamicMap widgetBuilderScope,
DynamicMap? initialState,
) : super(fullName, arguments, widgetBuilderScope, initialState);
final _CurriedWidget child;
@override
Widget buildChild(BuildContext context, DataSource source, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
Widget buildChild(
BuildContext context,
DataSource source,
DynamicContent data,
RemoteEventHandler remoteEventTarget,
List<_WidgetState> states,
_StateResolverCallback stateResolver,
_DataResolverCallback dataResolver,
_WidgetBuilderArgResolverCallback widgetBuilderArgResolver,
) {
return child.build(context, data, remoteEventTarget, states);
}
@ -808,13 +1093,34 @@ class _CurriedRemoteWidget extends _CurriedWidget {
}
class _CurriedSwitch extends _CurriedWidget {
const _CurriedSwitch(FullyQualifiedWidgetName fullName, this.root, DynamicMap arguments, DynamicMap? initialState) : super(fullName, arguments, initialState);
const _CurriedSwitch(
FullyQualifiedWidgetName fullName,
this.root,
DynamicMap arguments,
DynamicMap widgetBuilderScope,
DynamicMap? initialState,
) : super(fullName, arguments, widgetBuilderScope, initialState);
final Switch root;
@override
Widget buildChild(BuildContext context, DataSource source, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
final Object resolvedWidget = _CurriedWidget._resolveFrom(root, const <Object>[], stateResolver, dataResolver);
Widget buildChild(
BuildContext context,
DataSource source,
DynamicContent data,
RemoteEventHandler remoteEventTarget,
List<_WidgetState> states,
_StateResolverCallback stateResolver,
_DataResolverCallback dataResolver,
_WidgetBuilderArgResolverCallback widgetBuilderArgResolver,
) {
final Object resolvedWidget = _CurriedWidget._resolveFrom(
root,
const <Object>[],
stateResolver,
dataResolver,
widgetBuilderArgResolver,
);
if (resolvedWidget is _CurriedWidget) {
return resolvedWidget.build(context, data, remoteEventTarget, states);
}
@ -826,12 +1132,20 @@ class _CurriedSwitch extends _CurriedWidget {
}
class _Widget extends StatefulWidget {
const _Widget({ required this.curriedWidget, required this.data, required this.remoteEventTarget, required this.states });
const _Widget({
required this.curriedWidget,
required this.data,
required this.widgetBuilderScope,
required this.remoteEventTarget,
required this.states,
});
final _CurriedWidget curriedWidget;
final DynamicContent data;
final DynamicContent widgetBuilderScope;
final RemoteEventHandler remoteEventTarget;
final List<_WidgetState> states;
@ -1014,6 +1328,36 @@ class _WidgetState extends State<_Widget> implements DataSource {
];
}
@override
Widget builder(List<Object> argsKey, DynamicMap builderArg) {
return _fetchBuilder(argsKey, builderArg, optional: false)!;
}
@override
Widget? optionalBuilder(List<Object> argsKey, DynamicMap builderArg) {
return _fetchBuilder(argsKey, builderArg);
}
Widget? _fetchBuilder(
List<Object> argsKey,
DynamicMap builderArg, {
bool optional = true,
}) {
final Object value = _fetch(argsKey, expandLists: false);
if (value is _RemoteWidgetBuilder) {
final _CurriedWidget curriedWidget = value(builderArg);
return curriedWidget.build(
context,
widget.data,
widget.remoteEventTarget,
widget.states,
);
}
return optional
? null
: _buildErrorWidget('Not a builder at $argsKey (got $value) for ${widget.curriedWidget.fullName}.');
}
@override
VoidCallback? voidHandler(List<Object> argsKey, [ DynamicMap? extraArguments ]) {
return handler<VoidCallback>(argsKey, (HandlerTrigger callback) => () => callback(extraArguments));
@ -1064,7 +1408,13 @@ class _WidgetState extends State<_Widget> implements DataSource {
assert(!_debugFetching);
try {
_debugFetching = true;
final Object result = widget.curriedWidget.resolve(argsKey, _stateResolver, _dataResolver, expandLists: expandLists);
final Object result = widget.curriedWidget.resolve(
argsKey,
_stateResolver,
_dataResolver,
_widgetBuilderArgResolver,
expandLists: expandLists,
);
for (final _Subscription subscription in _dependencies) {
subscription.addClient(key);
}
@ -1095,6 +1445,17 @@ class _WidgetState extends State<_Widget> implements DataSource {
return subscription.value;
}
Object _widgetBuilderArgResolver(List<Object> rawDataKey) {
final _Key widgetBuilderArgKey = _Key(_kWidgetBuilderArgSection, rawDataKey);
final _Subscription subscription = _subscriptions[widgetBuilderArgKey] ??= _Subscription(
widget.widgetBuilderScope,
this,
rawDataKey,
);
_dependencies.add(subscription);
return subscription.value;
}
Object _stateResolver(List<Object> rawStateKey, int depth) {
final _Key stateKey = _Key(depth, rawStateKey);
final _Subscription subscription;
@ -1126,7 +1487,16 @@ class _WidgetState extends State<_Widget> implements DataSource {
@override
Widget build(BuildContext context) {
// TODO(ianh): what if this creates some _dependencies?
return widget.curriedWidget.buildChild(context, this, widget.data, widget.remoteEventTarget, _states, _stateResolver, _dataResolver);
return widget.curriedWidget.buildChild(
context,
this,
widget.data,
widget.remoteEventTarget,
_states,
_stateResolver,
_dataResolver,
_widgetBuilderArgResolver,
);
}
@override
@ -1138,6 +1508,7 @@ class _WidgetState extends State<_Widget> implements DataSource {
const int _kDataSection = -1;
const int _kArgsSection = -2;
const int _kWidgetBuilderArgSection = -3;
@immutable
class _Key {

View File

@ -2,7 +2,7 @@ name: rfw
description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime."
repository: https://github.com/flutter/packages/tree/main/packages/rfw
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+rfw%22
version: 1.0.24
version: 1.0.25
environment:
sdk: ^3.2.0

View File

@ -503,4 +503,41 @@ void main() {
expect((value.widgets.first.root as ConstructorCall).name, 'c');
expect((value.widgets.first.root as ConstructorCall).arguments, isEmpty);
});
testWidgets('Library encoder: widget builders work', (WidgetTester tester) async {
const String source = '''
widget Foo = Builder(
builder: (scope) => Text(text: scope.text),
);
''';
final RemoteWidgetLibrary library = parseLibraryFile(source);
final Uint8List encoded = encodeLibraryBlob(library);
final RemoteWidgetLibrary decoded = decodeLibraryBlob(encoded);
expect(library.toString(), decoded.toString());
});
testWidgets('Library encoder: widget builders throws', (WidgetTester tester) async {
const RemoteWidgetLibrary remoteWidgetLibrary = RemoteWidgetLibrary(
<Import>[],
<WidgetDeclaration>[
WidgetDeclaration(
'a',
<String, Object?>{},
ConstructorCall(
'c',
<String, Object?>{
'builder': WidgetBuilderDeclaration('scope', ArgsReference(<Object>[])),
},
),
),
],
);
try {
decodeLibraryBlob(encodeLibraryBlob(remoteWidgetLibrary));
fail('did not throw exception');
} on FormatException catch (e) {
expect('$e', contains('Unrecognized data type 0x0A while decoding widget builder blob.'));
}
});
}

View File

@ -1088,4 +1088,470 @@ void main() {
data.update('c', 'test');
expect(log, <String>['leaf: 2', 'root: {a: [2, 3], b: [q, r]}', 'root: {a: [2, 3], b: [q, r], c: test}']);
});
testWidgets('Data source - optional builder works', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'Builder': (BuildContext context, DataSource source) {
final Widget? builder = source.optionalBuilder(<String>['builder'], <String, Object?>{});
return builder ?? const Text('Hello World!', textDirection: TextDirection.ltr);
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test = Builder(
builder: Text(text: 'Not a builder :/'),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
final Finder textFinder = find.byType(Text);
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, 'Hello World!');
});
testWidgets('Data source - builder returns an error widget', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
const String expectedErrorMessage = 'Not a builder at [builder] (got core:Text {} {text: Not a builder :/}) for local:Builder.';
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'Builder': (BuildContext context, DataSource source) {
return source.builder(<String>['builder'], <String, Object?>{});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test = Builder(
builder: Text(text: 'Not a builder :/'),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(tester.takeException().toString(), contains(expectedErrorMessage));
expect(find.byType(ErrorWidget), findsOneWidget);
expect(tester.widget<ErrorWidget>(find.byType(ErrorWidget)).message, expectedErrorMessage);
});
testWidgets('Widget builders - work when scope is not used', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'Builder': (BuildContext context, DataSource source) {
return source.builder(<String>['builder'], <String, Object?>{});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test = Builder(
builder: (scope) => Text(text: 'Hello World!', textDirection: 'ltr'),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, 'Hello World!');
});
testWidgets('Widget builders - work when scope is used', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'HelloWorld': (BuildContext context, DataSource source) {
const String result = 'Hello World!';
return source.builder(<String>['builder'], <String, Object?>{'result': result});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test = HelloWorld(
builder: (result) => Text(text: result.result, textDirection: 'ltr'),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, 'Hello World!');
});
testWidgets('Widget builders - work with state', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'IntToString': (BuildContext context, DataSource source) {
final int value = source.v<int>(<String>['value'])!;
final String result = value.toString();
return source.builder(<String>['builder'], <String, Object?>{'result': result});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test {value: 0} = IntToString(
value: state.value,
builder: (result) => Text(text: result.result, textDirection: 'ltr'),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, '0');
});
testWidgets('Widget builders - work with data', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent(<String, Object>{'value': 0});
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'IntToString': (BuildContext context, DataSource source) {
final int value = source.v<int>(<String>['value'])!;
final String result = value.toString();
return source.builder(<String>['builder'], <String, Object?>{'result': result});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test = IntToString(
value: data.value,
builder: (result) => Text(text: result.result, textDirection: 'ltr'),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, '0');
data.update('value', 1);
await tester.pump();
expect(tester.widget<Text>(textFinder).data, '1');
});
testWidgets('Widget builders - work with events', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
final List<RfwEvent> dispatchedEvents = <RfwEvent>[];
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'Zero': (BuildContext context, DataSource source) {
return source.builder(<String>['builder'], <String, Object?>{'result': 0});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test = Zero(
builder: (result) => GestureDetector(
onTap: event 'works' {number: result.result},
child: Text(text: 'Tap to trigger an event.', textDirection: 'ltr'),
),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
onEvent: (String eventName, DynamicMap eventArguments) =>
dispatchedEvents.add(RfwEvent(eventName, eventArguments)),
));
await tester.tap(textFinder);
await tester.pump();
expect(dispatchedEvents, hasLength(1));
expect(dispatchedEvents.single.name, 'works');
expect(dispatchedEvents.single.arguments['number'], 0);
});
testWidgets('Widget builders - works nested', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'Sum': (BuildContext context, DataSource source) {
final int operand1 = source.v<int>(<String>['operand1'])!;
final int operand2 = source.v<int>(<String>['operand2'])!;
final int result = operand1 + operand2;
return source.builder(<String>['builder'], <String, Object?>{'result': result});
},
'IntToString': (BuildContext context, DataSource source) {
final int value = source.v<int>(<String>['value'])!;
final String result = value.toString();
return source.builder(<String>['builder'], <String, Object?>{'result': result});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test = Sum(
operand1: 1,
operand2: 2,
builder: (result1) => IntToString(
value: result1.result,
builder: (result2) => Text(text: ['1 + 2 = ', result2.result], textDirection: 'ltr'),
),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, '1 + 2 = 3');
});
testWidgets('Widget builders - works nested dynamically', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Map<String, VoidCallback> handlers = <String, VoidCallback>{};
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent(<String, Object?>{
'a1': 'apricot',
'b1': 'blueberry',
});
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'Builder': (BuildContext context, DataSource source) {
final String? id = source.v<String>(<String>['id']);
if (id != null) {
handlers[id] = source.voidHandler(<String>['handler'])!;
}
return source.builder(<String>['builder'], <String, Object?>{
'param1': source.v<String>(<String>['arg1']),
'param2': source.v<String>(<String>['arg2']),
});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test { state1: 'strawberry' } = Builder(
arg1: data.a1,
arg2: 'apple',
id: 'A',
handler: set state.state1 = 'STRAWBERRY',
builder: (builder1) => Builder(
arg1: data.b1,
arg2: 'banana',
builder: (builder2) => Text(
textDirection: 'ltr',
text: [
state.state1, ' ', builder1.param1, ' ', builder1.param2, ' ', builder2.param1, ' ', builder2.param2,
],
),
),
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(tester.widget<Text>(textFinder).data, 'strawberry apricot apple blueberry banana');
data.update('a1', 'APRICOT');
await tester.pump();
expect(tester.widget<Text>(textFinder).data, 'strawberry APRICOT apple blueberry banana');
data.update('b1', 'BLUEBERRY');
await tester.pump();
expect(tester.widget<Text>(textFinder).data, 'strawberry APRICOT apple BLUEBERRY banana');
handlers['A']!();
await tester.pump();
expect(tester.widget<Text>(textFinder).data, 'STRAWBERRY APRICOT apple BLUEBERRY banana');
});
testWidgets('Widget builders - switch works with builder', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'Builder': (BuildContext context, DataSource source) {
return source.builder(<String>['builder'], <String, Object?>{});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test {enabled: false} = Builder(
value: state.value,
builder: switch state.enabled {
true: (scope) => GestureDetector(
onTap: set state.enabled = false,
child: Text(text: 'The builder is enabled.', textDirection: 'ltr'),
),
false: (scope) => GestureDetector(
onTap: set state.enabled = true,
child: Text(text: 'The builder is disabled.', textDirection: 'ltr'),
),
},
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, 'The builder is disabled.');
await tester.tap(textFinder);
await tester.pump();
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, 'The builder is enabled.');
});
testWidgets('Widget builders - builder works with switch', (WidgetTester tester) async {
const LibraryName coreLibraryName = LibraryName(<String>['core']);
const LibraryName localLibraryName = LibraryName(<String>['local']);
const LibraryName remoteLibraryName = LibraryName(<String>['remote']);
final Runtime runtime = Runtime();
final DynamicContent data = DynamicContent();
final Finder textFinder = find.byType(Text);
runtime.update(coreLibraryName, createCoreWidgets());
runtime.update(localLibraryName, LocalWidgetLibrary(<String, LocalWidgetBuilder> {
'Inverter': (BuildContext context, DataSource source) {
final bool value = source.v<bool>(<String>['value'])!;
return source.builder(<String>['builder'], <String, Object?>{'result': !value});
},
}));
runtime.update(remoteLibraryName, parseLibraryFile('''
import core;
import local;
widget test {value: false} = Inverter(
value: state.value,
builder: (result) => switch result.result {
true: GestureDetector(
onTap: set state.value = switch state.value {
true: false,
false: true,
},
child: Text(text: 'The input is false, the output is true', textDirection: 'ltr'),
),
false: GestureDetector(
onTap: set state.value = switch state.value {
true: false,
false: true,
},
child: Text(text: 'The input is true, the output is false', textDirection: 'ltr'),
),
},
);
'''));
await tester.pumpWidget(RemoteWidget(
runtime: runtime,
data: data,
widget: const FullyQualifiedWidgetName(remoteLibraryName, 'test'),
));
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, 'The input is false, the output is true');
await tester.tap(textFinder);
await tester.pump();
expect(textFinder, findsOneWidget);
expect(tester.widget<Text>(textFinder).data, 'The input is true, the output is false');
});
}
final class RfwEvent {
RfwEvent(this.name, this.arguments);
final String name;
final DynamicMap arguments;
}

View File

@ -340,4 +340,145 @@ void main() {
final RemoteWidgetLibrary result = parseLibraryFile('widget a {b: 0} = c();');
expect(result.widgets.single.initialState, <String, Object?>{'b': 0});
});
testWidgets('parseLibraryFile: widgetBuilders work', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a = Builder(builder: (scope) => Container());
''');
expect(libraryFile.toString(), 'widget a = Builder({builder: (scope) => Container({})});');
});
testWidgets('parseLibraryFile: widgetBuilders work with arguments', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a = Builder(builder: (scope) => Container(width: scope.width));
''');
expect(libraryFile.toString(), 'widget a = Builder({builder: (scope) => Container({width: scope.width})});');
});
testWidgets('parseLibraryFile: widgetBuilder arguments are lexical scoped', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a = A(
a: (s1) => B(
b: (s2) => T(s1: s1.s1, s2: s2.s2),
),
);
''');
expect(libraryFile.toString(), 'widget a = A({a: (s1) => B({b: (s2) => T({s1: s1.s1, s2: s2.s2})})});');
});
testWidgets('parseLibraryFile: widgetBuilder arguments can be shadowed', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a = A(
a: (s1) => B(
b: (s1) => T(t: s1.foo),
),
);
''');
expect(libraryFile.toString(), 'widget a = A({a: (s1) => B({b: (s1) => T({t: s1.foo})})});');
});
testWidgets('parseLibraryFile: widgetBuilders check the returned value', (WidgetTester tester) async {
void test(String input, String expectedMessage) {
try {
parseLibraryFile(input);
fail('parsing `$input` did not result in an error (expected "$expectedMessage").');
} on ParserException catch (e) {
expect('$e', expectedMessage);
}
}
const String expectedErrorMessage =
'Expecting a switch or constructor call got 1 at line 1 column 27.';
test('widget a = B(b: (foo) => 1);', expectedErrorMessage);
});
testWidgets('parseLibraryFile: widgetBuilders check reserved words', (WidgetTester tester) async {
void test(String input, String expectedMessage) {
try {
parseLibraryFile(input);
fail('parsing `$input` did not result in an error (expected "$expectedMessage").');
} on ParserException catch (e) {
expect('$e', expectedMessage);
}
}
const String expectedErrorMessage =
'args is a reserved word at line 1 column 34.';
test('widget a = Builder(builder: (args) => Container(width: args.width));', expectedErrorMessage);
});
testWidgets('parseLibraryFile: widgetBuilders check reserved words', (WidgetTester tester) async {
void test(String input, String expectedMessage) {
try {
parseDataFile(input);
fail('parsing `$input` did not result in an error (expected "$expectedMessage").');
} on ParserException catch (e) {
expect('$e', expectedMessage);
}
}
const String expectedErrorMessage =
'Expected symbol "{" but found widget at line 1 column 7.';
test('widget a = Builder(builder: (args) => Container(width: args.width));', expectedErrorMessage);
});
testWidgets('parseLibraryFile: switch works with widgetBuilders', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a = A(
b: switch args.down {
true: (foo) => B(),
false: (bar) => C(),
}
);
''');
expect(libraryFile.toString(), 'widget a = A({b: switch args.down {true: (foo) => B({}), false: (bar) => C({})}});');
});
testWidgets('parseLibraryFile: widgetBuilders work with switch', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a = A(
b: (foo) => switch foo.letter {
'a': A(),
'b': B(),
},
);
''');
expect(libraryFile.toString(), 'widget a = A({b: (foo) => switch foo.letter {a: A({}), b: B({})}});');
});
testWidgets('parseLibraryFile: widgetBuilders work with lists', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a = A(
b: (s1) => B(c: [s1.c]),
);
''');
expect(libraryFile.toString(), 'widget a = A({b: (s1) => B({c: [s1.c]})});' );
});
testWidgets('parseLibraryFile: widgetBuilders work with maps', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a = A(
b: (s1) => B(c: {d: s1.d}),
);
''');
expect(libraryFile.toString(), 'widget a = A({b: (s1) => B({c: {d: s1.d}})});');
});
testWidgets('parseLibraryFile: widgetBuilders work with setters', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a {foo: 0} = A(
b: (s1) => B(onTap: set state.foo = s1.foo),
);
''');
expect(libraryFile.toString(), 'widget a = A({b: (s1) => B({onTap: set state.foo = s1.foo})});');
});
testWidgets('parseLibraryFile: widgetBuilders work with events', (WidgetTester tester) async {
final RemoteWidgetLibrary libraryFile = parseLibraryFile('''
widget a {foo: 0} = A(
b: (s1) => B(onTap: event "foo" {result: s1.result})
);
''');
expect(libraryFile.toString(), 'widget a = A({b: (s1) => B({onTap: event foo {result: s1.result}})});');
});
}