mirror of
https://github.com/flutter/packages.git
synced 2025-06-30 14:47:22 +08:00
[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:
@ -1,3 +1,6 @@
|
||||
## 1.0.25
|
||||
* Adds support for wildget builders.
|
||||
|
||||
## 1.0.24
|
||||
|
||||
* Adds `InkResponse` material widget.
|
||||
|
@ -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);
|
||||
|
@ -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].
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}})});');
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user