The runtime model This section describes how remote widget libraries in Remote Flutter Widgets work. For clarity, some examples are provided using the Remote Flutter Widgets library text format. Library names 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. Dependencies 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. The Remote Flutter Widgets package ships with two local widget libraries, core.widgets and core.material, which are described below. 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 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. In the text format, the imports come at the top of the file. 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, as defined below. In some cases, widgets can also declare initial state; see State below. The widget declarations come after the imports. Here a "Foo" widget is declared that just creates a "Bar" widget: Foo = Bar(); 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, or state, switches or loops, or event handlers. In this example, several constructor calls are nested together: Foo = Column( children: [ Container( child: Text(text: "Hello"), ), ], ); 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: Foo = Text(text: args.name); Arguments can be structured, as described in the previous section. Argument references are lists of strings and integers which form a path to look up leaves in that tree. 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". This is typically expressed as args.show.phrase. For example: 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. Data model references are lists of strings and integers which form a path to look up leaves in that tree. 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". This is typically expressed as data.server.cart.1.name. For example: Text(text: data.server.cart.1.name) 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). 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 a button is described as having a "down" state whose first value is "false": Button { down: false } = Container( // ... ); 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 args, data, and state references. In the text format, a widget that shows all the values from a list in a ListView might look like this: Items = ListView( children: [ ...for item in args.list: Text(item), ], ); Such a widget would be used like this: Items(list: [ "Hello", "World" ]) 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 an args, data, state, or loop reference. 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!): 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 an event Event handlers that signal an event have a name and a map. 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: "Hello"), ); Setting state Event handlers that set state have a reference to a state, and a new value to assign to that state. Obviously, they're only functional within widgets that have state, as described above. This lets us finish the earlier button: 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, ), );