parseLibraryFile function
Parses a Remote Flutter Widgets text library file.
Remote widget libraries are usually used in conjunction with a Runtime.
Parsing this format is about ten times slower than parsing the binary variant; see decodeLibraryBlob. As such it is strongly discouraged, especially in resource-constrained contexts like mobile applications.
Format
The format is a superset of the format defined by parseDataFile.
Remote Flutter Widgets text library files consist of a list of imports followed by a list of widget declarations.
Imports
A remote widget library file is identified by a name which consists of
several parts, which are by convention expressed separated by periods; for
example, core.widgets
or net.example.x
.
A library's name is specified when the library is provided to the runtime using Runtime.update.
A remote widget library depends on one or more other libraries that define the widgets that the primary library depends on. These dependencies can themselves be remote widget libraries, for example describing commonly-used widgets like branded buttons, or "local widget libraries" which are declared and hard-coded in the client itself and that provide a way to reference actual Flutter widgets (see LocalWidgetLibrary).
The Remote Flutter Widgets package ships with two local widget libraries,
usually given the names core.widgets
(see createCoreWidgets) and
core.material
(see createMaterialWidgets). An application can declare
other local widget libraries for use by their remote widgets. These could
correspond to UI controls, e.g. branded widgets used by other parts of the
application, or to complete experiences, e.g. core parts of the application.
For example, a blogging application might use Remote Flutter Widgets to
represent the CRM parts of the experience, with the rich text editor being
implemented on the client as a custom widget exposed to the remote libraries
as a widget in a local widget library.
A library lists the other libraries that it depends on by name. When a widget is referenced, it is looked up by name first by examining the widget declarations in the file itself, then by examining the declarations of each dependency in turn, in a depth-first search.
It is an error for there to be a loop in the imports.
Imports have this form:
import library.name;
For example:
import core.widgets;
Widget declarations
The primary purpose of a remote widget library is to provide widget declarations. Each declaration defines a new widget. Widgets are defined in terms of other widgets, like stateless and stateful widgets in Flutter itself. As such, a widget declaration consists of a widget constructor call.
The widget declarations come after the imports.
To declare a widget named A in terms of a widget B, the following form is used:
widget A = B();
This declares a widget A, whose implementation is simply to use widget B.
If the widget A is to be stateful, a map is inserted before the equals sign:
widget A { } = B();
The map describes the default values of the state. For example, a button might have a "down" state, which is initially false:
widget Button { down: false } = Container();
See the section on State below.
Widget constructor calls
A widget constructor call is an invocation of a remote or local widget 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, widget builders, loops, state, switches or event handlers.
In this example, several constructor calls are nested together:
widget Foo = Column(
children: [
Container(
child: Text(text: "Hello"),
),
Builder(
builder: (scope) => Text(text: scope.world),
),
],
);
The Foo
widget is defined to create a Column
widget. The Column(...)
is a constructor call with one argument, named children
, whose value is a
list which itself contains a single constructor call, to Container
. That
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 arguments of a widget, or to the DynamicContent data, or to a stateful widget's state.
The various kinds of references all have the same basic pattern, a prefix followed by period-separated identifiers, strings, or integers. Identifiers and strings are used to index into maps, while integers are used to index into lists.
For example, "foo.2.fruit" would reference the key with the value "Kiwi" in the following structure:
{
foo: [
{ fruit: "Apple" },
{ fruit: "Banana" },
{ fruit: "Kiwi" }, // foo.2.fruit is the string "Kiwi" here
],
bar: [ ],
}
Peferences to a widget's arguments use "args" as the first component:
args.foo.bar
References to the data model use use "data" as the first component, and references to state use "state" as the first component:
data.foo.bar
state.foo.bar
Finally, references to loop variables use the identifier specified in the loop as the first component:
ident.foo.bar
Argument references
Instead of passing literal values as arguments in widget constructor calls, a reference to one of the arguments of the remote widget being defined itself can be provided instead.
For example, suppose one instantiated a widget Foo as follows:
Foo(name: "Bobbins")
...then in the definition of Foo, one might pass the value of this "name" argument to another widget, say a Text widget, as follows:
widget Foo = Text(text: args.name);
The arguments can have structure. For example, if the argument passed to Foo was:
Foo(show: { name: "Cracking the Cryptic", phrase: "Bobbins" })
...then to specify the leaf node whose value is the string "Bobbins", one
would specify an argument reference consisting of the values "show" and
"phrase", as in args.show.phrase
. For example:
widget Foo = Text(text: args.show.phrase);
Data model references
Instead of passing literal values as arguments in widget constructor calls, or references to one's own arguments, a reference to one of the nodes in the data model can be provided instead.
The data model is a tree of maps and lists with leaves formed of integers, doubles, bools, and strings (see DynamicContent). For example, if the data model looks like this:
{ server: { cart: [ { name: "Apple"}, { name: "Banana"} ] }
...then to specify the leaf node whose value is the string "Banana", one
would specify a data model reference consisting of the values "server",
"cart", 1, and "name", as in data.server.cart.1.name
. For example:
Text(text: data.server.cart.1.name)
Loops
In a list, a loop can be employed to map another list into the host list, mapping values of the embedded list according to a provided template. Within the template, references to the value from the embedded list being expanded can be provided using a loop reference, which is similar to argument and data references.
A widget that shows all the values from a list in a ListView might look like this:
widget Items = ListView(
children: [
...for item in args.list:
Text(text: item),
],
);
Such a widget would be used like this:
Items(list: [ "Hello", "World" ])
The syntax for a loop uses the following form:
...for ident in list: template
...where ident is the identifier to bind to each value in the list, list is some value that evaluates to a list, and template is a value that is to be evaluated for each item in list.
This loop syntax is only valid inside lists.
Loop references use the ident. In the example above, that is item
. In
more elaborate examples, it can include subreferences. For example:
widget Items = ListView(
children: [
Text(text: 'Products:'),
...for item in args.products:
Text(text: product.name.displayName),
Text(text: 'End of list.'),
],
);
This might be used as follows:
Items(products: [
{ name: { abbreviation: "TI4", displayName: "Twilight Imperium IV" }, price: 120.0 },
{ name: { abbreviation: "POK", displayName: "Prophecy of Kings" }, price: 100.0 },
])
State
A widget declaration can say that it has an "initial state", the structure of which is the same as the data model structure (maps and lists of primitive types, the root is a map).
Here a button is described as having a "down" state whose first value is "false":
widget Button { down: false } = Container(
// ...
);
If a widget has state, then it can be referenced in the same way as the widget's arguments and the data model can be referenced, and it can be changed using event handlers as described below.
Here, the button's state is referenced (in a pretty nonsensical way; controlling whether its label wraps based on the value of the state):
widget Button { down: false } = Container(
child: Text(text: 'Hello World', softWrap: state.down),
);
State is usually used with Switches and state-setting handlers.
Switches
Anywhere in a widget declaration, a switch can be employed to change the evaluated value used at runtime. A switch has a value that is being used to control the switch, and then a series of cases with values to use if the control value matches the case value. A default can be provided.
The control value is usually a reference to arguments, data, state, or a loop variable.
The syntax for a switch uses the following form:
switch value {
case1: template1,
case2: template2,
case3: template3,
// ...
default: templateD,
}
...where value is the control value that will be compared to each case, the caseX values are the values to which the control value is compared, templateX are the templates to use, and templateD is the default template to use if none of the cases can be met. Any number of cases can be specified; the template of the first one that exactly matches the given control value is the one that is used. The default entry is optional. If no value matches and there is no default, the switch evaluates to the "missing" value (null).
Extending the earlier button, this would move the margin around so that it appeared pressed when the "down" state was true (but note that we still don't have anything to toggle that state!):
widget Button { down: false } = Container(
margin: switch state.down {
false: [ 0.0, 0.0, 8.0, 8.0 ],
true: [ 8.0, 8.0, 0.0, 0.0 ],
},
decoration: { type: "box", border: [ {} ] },
child: args.child,
);
Event handlers
There are two kinds of event handlers: those that signal an event for the host to handle (potentially by forwarding it to a server), and those that change the widget's state.
Signalling event handlers have a name and an arguments map:
event "..." { }
The string is the name of the event, and the arguments map is the data to send with the event.
For example, the event handler in the following sequence sends the event called "hello" with a map containing just one key, "id", whose value is 1:
Button(
onPressed: event "hello" { id: 1 },
child: Text(text: "Greetings"),
);
Event handlers that set state have a reference to a state, and a new value to assign to that state. Such handlers are only meaningful within widgets that have state, as described above. They have this form:
set state.foo.bar = value
The state.foo.bar
part is a state reference (which must identify a part of
the state that exists), and value
is the new value to assign to that state.
This lets us finish the earlier button:
widget Button { down: false } = GestureDetector(
onTapDown: set state.down = true,
onTapUp: set state.down = false,
onTapCancel: set state.down = false,
onTap: args.onPressed,
child: Container(
margin: switch state.down {
false: [ 0.0, 0.0, 8.0, 8.0 ],
true: [ 8.0, 8.0, 0.0, 0.0 ],
},
decoration: { type: "box", border: [ {} ] },
child: args.child,
),
);
Source identifier
The optional sourceIdentifier
parameter can be provided to cause the
parser to track source locations for BlobNode subclasses included in the
parsed subtree. If the value is null (the default), then source locations
are not tracked. If the value is non-null, it is used as the value of the
SourceLocation.source for each SourceRange of each BlobNode.source.
See also:
- encodeLibraryBlob, which encodes the output of this method into the binary variant of this format.
- parseDataFile, which uses a subset of this format to decode Remote Flutter Widgets text data files.
- decodeLibraryBlob, which decodes the binary variant of this format.
Implementation
RemoteWidgetLibrary parseLibraryFile(String file, { Object? sourceIdentifier }) {
final _Parser parser = _Parser(_tokenize(file), sourceIdentifier);
return parser.readLibraryFile();
}