| # Remote Flutter Widgets |
| |
| This package provides a mechanism for rendering widgets based on |
| declarative UI descriptions that can be obtained at runtime. |
| |
| ## Status |
| |
| This package is relatively stable. |
| |
| We plan to keep the format and supported widget set backwards compatible, |
| so that once a file works, it will keep working. _However_, this is best-effort |
| only. To guarantee that files keep working as you expect, submit |
| tests to this package (e.g. the binary file and the corresponding screenshot, |
| as a golden test). |
| |
| The set of widgets supported by this package is somewhat arbitrary. |
| PRs that add new widgets from Flutter's default widget libraries |
| (`widgets`, `material`, and`'cupertino`) are welcome. |
| |
| There are some known theoretical performance limitations with the |
| package's current implementation, but so far nobody has reported |
| experiencing them in production. Please [file |
| issues](https://github.com/flutter/flutter/issues/new?labels=p:%20rfw,package,P2) |
| if you run into them. |
| |
| ## Feedback |
| |
| We would love to hear your experiences using this package, whether |
| positive or negative. In particular, stories of uses of this package |
| in production would be very interesting. Please add comments to [issue |
| 90218](https://github.com/flutter/flutter/issues/90218). |
| |
| ## Limitations |
| |
| Once you realize that you can ship UI (and maybe logic, e.g. using |
| Wasm; see the example below) you will slowly be tempted to move your |
| whole application to this model. |
| |
| This won't work. |
| |
| Flutter proper lets you create widgets for compelling UIs with |
| gestures and animations and so forth. With RFW you can use those |
| widgets, but it doesn't let you _create_ those widgets. |
| |
| For example, you don't want to use RFW to create a UI that involves |
| page transitions. You don't want to use RFW to create new widgets that |
| involve drag and drop. You don't want to use RFW to create widgets |
| that involve custom painters. |
| |
| Rather, RFW is best suited for interfaces made out of prebuilt |
| components. For example, a database front-end could use this to |
| describe bespoke UIs for editing different types of objects in the |
| database. Message-of-the-day announcements could be built using this |
| mechanism. Search interfaces could use this mechanism for rich result |
| cards. |
| |
| RFW is well-suited for describing custom UIs from a potentially |
| infinite set of UIs that could not possibly have been known when the |
| application was created. On the other hand, updating the application's |
| look and feel, changing how navigation works in an application, or |
| adding new features, are all changes that are best made in Flutter |
| itself, creating a new application and shipping that through normal |
| update channels. |
| |
| ## Using Remote Flutter Widgets |
| |
| ### Introduction |
| |
| The Remote Flutter Widgets (RFW) package combines widget descriptions |
| obtained at runtime, data obtained at runtime, some predefined widgets |
| provided at compile time, and some app logic provided at compile time |
| (possibly combined with other packages to enable new logic to be |
| obtained at runtime), to generate arbitrary widget trees at runtime. |
| |
| The widget descriptions obtained at runtime (e.g. over the network) |
| are called _remote widget libraries_. These are normally transported |
| in a binary format with the file extension `.rfw`. They can be written |
| in a text format (file extension `.rfwtxt`), and either used directly |
| or converted into the binary format for transport. The `rfw` package |
| provides APIs for parsing and encoding these formats. The |
| [parts of the package](https://pub.dev/documentation/rfw/latest/formats/formats-library.html) |
| that only deal with these formats can be imported directly and have no |
| dependency on Flutter's `dart:ui` library, which means they can be |
| used on the server or in command-line applications. |
| |
| The data obtained at runtime is known as _configuration data_ and is |
| represented by `DynamicContent` objects. It uses a data structure |
| similar to JSON (but it distinguishes `int` and `double` and does not |
| support `null`). The `rfw` package provides both binary and text |
| formats to carry this data; JSON can also be used directly (with some |
| caution), and the data can be created directly in Dart. This is |
| discussed in more detail in the |
| [DynamicContent](https://pub.dev/documentation/rfw/latest/rfw/DynamicContent-class.html) |
| API documentation. |
| |
| Remote widget libraries can use the configuration data to define how |
| the widgets are built. |
| |
| Remote widget libraries all eventually bottom out in the predefined |
| widgets that are compiled into the application. These are called |
| _local widget libraries_. The `rfw` package ships with two local |
| widget libraries, the [core |
| widgets](https://pub.dev/documentation/rfw/latest/rfw/createCoreWidgets.html) |
| from the `widgets` library (such as `Text`, `Center`, `Row`, etc), and |
| some of the [material |
| widgets](https://pub.dev/documentation/rfw/latest/rfw/createMaterialWidgets.html). |
| |
| Programs can define their own local widget libraries, to provide more |
| widgets for remote widget libraries to use. |
| |
| These components are combined using a |
| [`RemoteWidget`](https://pub.dev/documentation/rfw/latest/rfw/RemoteWidget-class.html) |
| widget and a |
| [`Runtime`](https://pub.dev/documentation/rfw/latest/rfw/Runtime-class.html) |
| object. |
| |
| The remote widget libraries can specify _events_ that trigger in |
| response to callbacks. For example, the `OutlinedButton` widget |
| defined in the |
| [Material](https://pub.dev/documentation/rfw/latest/rfw/createMaterialWidgets.html) |
| local widget library has an `onPressed` property which the remote |
| widget library can define as triggering an event. Events can contain |
| data (either hardcoded or obtained from the configuration data). |
| |
| These events result in a callback on the `RemoteWidget` being invoked. |
| Events can either have hardcoded results, or the `rfw` package can be |
| combined with other packages such as |
| [`wasm_run_flutter`](https://pub.dev/packages/wasm_run_flutter) so |
| that events trigger code obtained at runtime. That code typically |
| changes the configuration data, resulting in an update to the rendered |
| widgets. |
| |
| _See also: [API documentation](https://pub.dev/documentation/rfw/latest/rfw/rfw-library.html)_ |
| |
| ### Getting started |
| |
| A Flutter application can render remote widgets using the |
| `RemoteWidget` widget, as in the following snippet: |
| |
| <?code-excerpt "example/hello/lib/main.dart (Example)"?> |
| ```dart |
| class Example extends StatefulWidget { |
| const Example({super.key}); |
| |
| @override |
| State<Example> createState() => _ExampleState(); |
| } |
| |
| class _ExampleState extends State<Example> { |
| final Runtime _runtime = Runtime(); |
| final DynamicContent _data = DynamicContent(); |
| |
| // Normally this would be obtained dynamically, but for this example |
| // we hard-code the "remote" widgets into the app. |
| // |
| // Also, normally we would decode this with [decodeLibraryBlob] rather than |
| // parsing the text version using [parseLibraryFile]. However, to make it |
| // easier to demo, this uses the slower text format. |
| static final RemoteWidgetLibrary _remoteWidgets = parseLibraryFile(''' |
| // The "import" keyword is used to specify dependencies, in this case, |
| // the built-in widgets that are added by initState below. |
| import core.widgets; |
| // The "widget" keyword is used to define a new widget constructor. |
| // The "root" widget is specified as the one to render in the build |
| // method below. |
| widget root = Container( |
| color: 0xFF002211, |
| child: Center( |
| child: Text(text: ["Hello, ", data.greet.name, "!"], textDirection: "ltr"), |
| ), |
| ); |
| '''); |
| |
| static const LibraryName coreName = LibraryName(<String>['core', 'widgets']); |
| static const LibraryName mainName = LibraryName(<String>['main']); |
| |
| @override |
| void initState() { |
| super.initState(); |
| // Local widget library: |
| _runtime.update(coreName, createCoreWidgets()); |
| // Remote widget library: |
| _runtime.update(mainName, _remoteWidgets); |
| // Configuration data: |
| _data.update('greet', <String, Object>{'name': 'World'}); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return RemoteWidget( |
| runtime: _runtime, |
| data: _data, |
| widget: const FullyQualifiedWidgetName(mainName, 'root'), |
| onEvent: (String name, DynamicMap arguments) { |
| // The example above does not have any way to trigger events, but if it |
| // did, they would result in this callback being invoked. |
| debugPrint('user triggered event "$name" with data: $arguments'); |
| }, |
| ); |
| } |
| } |
| ``` |
| |
| In this example, the "remote" widgets are hardcoded into the |
| application (`_remoteWidgets`), the configuration data is hardcoded |
| and unchanging (`_data`), and the event handler merely prints a |
| message to the console. |
| |
| In typical usage, the remote widgets come from a server at runtime, |
| either through HTTP or some other network transport. Separately, the |
| `DynamicContent` data would be updated, either from the server or |
| based on local data. |
| |
| Similarly, events that are signalled by the user's interactions with |
| the remote widgets (`RemoteWidget.onEvent`) would typically be sent to |
| the server for the server to update the data, or would cause the data |
| to be updated directly, on the user's device, according to some |
| predefined logic. |
| |
| It is recommended that servers send binary data, decoded using |
| `decodeLibraryBlob` and `decodeDataBlob`, when providing updates for |
| the remote widget libraries and data. |
| |
| ### Applying these concepts to typical use cases |
| |
| #### Message of the day, advertising, announcements |
| |
| When `rfw` is used for displaying content that is largely static in |
| presentation and updated only occasionally, the simplest approach is |
| to encode everything into the remote widget library, download that to |
| the client, and render it, with only minimal data provided in the |
| configuration data (e.g. the user's dark mode preference, their |
| username, the current date or time) and with a few predefined events |
| (such as one to signal the message should be closed and another to |
| signal the user checking a "do not show this again" checkbox, or |
| similar). |
| |
| #### Dynamic data editors |
| |
| A more elaborate use case might involve remote widget libraries being |
| used to describe the UI for editing structured data in a database. In |
| this case, the data may be more important, containing the current data |
| being edited, and the events may signal to the application how to |
| update the data on the backend. |
| |
| #### Search results |
| |
| A general search engine could have dedicated remote widgets defined |
| for different kinds of results, allowing the data to be formatted and |
| made interactive in ways that are specific to the query and in ways |
| that could not have been predicted when the application was created. |
| For example, new kinds of search results for current events could be |
| created on the fly and sent to the client without needing to update |
| the client application. |
| |
| ### Developing new local widget libraries |
| |
| A "local" widget library is one that describes the built-in widgets |
| that your "remote" widgets are built out of. The RFW package comes |
| with some preprepared libraries, available through |
| [createCoreWidgets](https://pub.dev/documentation/rfw/latest/rfw/createCoreWidgets.html) |
| and |
| [createMaterialWidgets](https://pub.dev/documentation/rfw/latest/rfw/createMaterialWidgets.html). |
| You can also create your own. |
| |
| When developing new local widget libraries, it is convenient to hook |
| into the `reassemble` method to update the local widgets. That way, |
| changes can be seen in real time when hot reloading. |
| |
| <?code-excerpt "example/local/lib/main.dart (Example)"?> |
| ```dart |
| class Example extends StatefulWidget { |
| const Example({super.key}); |
| |
| @override |
| State<Example> createState() => _ExampleState(); |
| } |
| |
| class _ExampleState extends State<Example> { |
| final Runtime _runtime = Runtime(); |
| final DynamicContent _data = DynamicContent(); |
| |
| @override |
| void initState() { |
| super.initState(); |
| _update(); |
| } |
| |
| @override |
| void reassemble() { |
| // This function causes the Runtime to be updated any time the app is |
| // hot reloaded, so that changes to _createLocalWidgets can be seen |
| // during development. This function has no effect in production. |
| super.reassemble(); |
| _update(); |
| } |
| |
| static WidgetLibrary _createLocalWidgets() { |
| return LocalWidgetLibrary(<String, LocalWidgetBuilder>{ |
| 'GreenBox': (BuildContext context, DataSource source) { |
| return Container( |
| color: const Color(0xFF002211), |
| child: source.child(<Object>['child']), |
| ); |
| }, |
| 'Hello': (BuildContext context, DataSource source) { |
| return Center( |
| child: Text( |
| 'Hello, ${source.v<String>(<Object>["name"])}!', |
| textDirection: TextDirection.ltr, |
| ), |
| ); |
| }, |
| }); |
| } |
| |
| static const LibraryName localName = LibraryName(<String>['local']); |
| static const LibraryName remoteName = LibraryName(<String>['remote']); |
| |
| void _update() { |
| _runtime.update(localName, _createLocalWidgets()); |
| // Normally we would obtain the remote widget library in binary form from a |
| // server, and decode it with [decodeLibraryBlob] rather than parsing the |
| // text version using [parseLibraryFile]. However, to make it easier to |
| // play with this sample, this uses the slower text format. |
| _runtime.update(remoteName, parseLibraryFile(''' |
| import local; |
| widget root = GreenBox( |
| child: Hello(name: "World"), |
| ); |
| ''')); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return RemoteWidget( |
| runtime: _runtime, |
| data: _data, |
| widget: const FullyQualifiedWidgetName(remoteName, 'root'), |
| onEvent: (String name, DynamicMap arguments) { |
| debugPrint('user triggered event "$name" with data: $arguments'); |
| }, |
| ); |
| } |
| } |
| ``` |
| |
| Widgets in local widget libraries are represented by closures that are |
| invoked by the runtime whenever a local widget is referenced. |
| |
| The closure uses the |
| [LocalWidgetBuilder](https://pub.dev/documentation/rfw/latest/rfw/LocalWidgetBuilder.html) |
| signature. Like any builder in Flutter, it takes a |
| [`BuildContext`](https://api.flutter.dev/flutter/widgets/BuildContext-class.html), |
| which can be used to look up inherited widgets. |
| |
| > For example, widgets that need the current text direction might |
| > defer to `Directionality.of(context)`, with the given `BuildContext` |
| > as the context argument. |
| |
| The other argument is a [`DataSource`](https://pub.dev/documentation/rfw/latest/rfw/DataSource-class.html). |
| This gives access to the arguments that were provided to the widget in |
| the remote widget library. |
| |
| For example, consider the example above, where the remote widget library is: |
| |
| <?code-excerpt "test/readme_test.dart (root)"?> |
| ```rfwtxt |
| import local; |
| widget root = GreenBox( |
| child: Hello(name: "World"), |
| ); |
| ``` |
| |
| The `GreenBox` widget is invoked with one argument (`child`), and the |
| `Hello` widget is invoked with one argument (`name`). |
| |
| In the definitions of `GreenBox` and `Hello`, the data source is used |
| to pull out these arguments. |
| |
| ### Obtaining arguments from the `DataSource` |
| |
| The arguments are a tree of maps and lists with leaves that are Dart |
| scalar values (`int`, `double`, `bool`, or `String`), further widgets, |
| or event handlers. |
| |
| #### Scalars |
| |
| Here is an example of a more elaborate widget argument: |
| |
| <?code-excerpt "test/readme_test.dart (fruit)"?> |
| ```rfwtxt |
| widget fruit = Foo( |
| bar: { quux: [ 'apple', 'banana', 'cherry' ] }, |
| ); |
| ``` |
| |
| To obtain a scalar value from the arguments, the |
| [DataSource.v](https://pub.dev/documentation/rfw/latest/rfw/DataSource/v.html) |
| method is used. This method takes a list of keys (strings or integers) |
| that denote the path to scalar in question. For instance, to obtain |
| "cherry" from the example above, the keys would be `bar`, `quux`, and |
| 2, as in: |
| |
| <?code-excerpt "test/readme_test.dart (v)"?> |
| ```dart |
| 'Foo': (BuildContext context, DataSource source) { |
| return Text(source.v<String>(<Object>['bar', 'quux', 2])!); |
| }, |
| ``` |
| |
| The `v` method is generic, with a type argument that specifies the |
| expected type (one of `int`, `double`, `bool`, or `String`). When the |
| value of the argument in the remote widget library does not match the |
| specified (or inferred) type given to `v`, or if the specified keys |
| don't lead to a value at all, it returns null instead. |
| |
| #### Maps and lists |
| |
| The `LocalWidgetBuilder` callback can inspect keys to see if they are |
| maps or lists before attempting to use them. For example, before |
| accessing a dozen fields from a map, one might use `isMap` to check if |
| the map is present at all. If it is not, then all the fields will be |
| null, and it is inefficient to fetch each one individually. |
| |
| The |
| [`DataSource.isMap`](https://pub.dev/documentation/rfw/latest/rfw/DataSource/isMap.html) |
| method is takes a list of keys (like `v`) and reports if the key |
| identifies a map. |
| |
| For example, in this case the `bar` argument can be treated either as |
| a map with a `name` subkey, or a scalar String: |
| |
| <?code-excerpt "test/readme_test.dart (isMap)"?> |
| ```dart |
| 'Foo': (BuildContext context, DataSource source) { |
| if (source.isMap(<Object>['bar'])) { |
| return Text('${source.v<String>(<Object>['bar', 'name'])}', textDirection: TextDirection.ltr); |
| } |
| return Text('${source.v<String>(<Object>['bar'])}', textDirection: TextDirection.ltr); |
| }, |
| ``` |
| |
| Thus either of the following would have the same result: |
| |
| <?code-excerpt "test/readme_test.dart (example1)"?> |
| ```rfwtxt |
| widget example1 = GreenBox( |
| child: Foo( |
| bar: 'Jean', |
| ), |
| ); |
| ``` |
| |
| <?code-excerpt "test/readme_test.dart (example2)"?> |
| ```rfwtxt |
| widget example2 = GreenBox( |
| child: Foo( |
| bar: { name: 'Jean' }, |
| ), |
| ); |
| ``` |
| |
| The |
| [`DataSource.isList`](https://pub.dev/documentation/rfw/latest/rfw/DataSource/isList.html) |
| method is similar but reports on whether the specified key identifies a list: |
| |
| <?code-excerpt "test/readme_test.dart (isList)"?> |
| ```dart |
| 'Foo': (BuildContext context, DataSource source) { |
| if (source.isList(<Object>['bar', 'quux'])) { |
| return Text('${source.v<String>(<Object>['bar', 'quux', 2])}', textDirection: TextDirection.ltr); |
| } |
| return Text('${source.v<String>(<Object>['baz'])}', textDirection: TextDirection.ltr); |
| }, |
| ``` |
| |
| For lists, a `LocalWidgetBuilder` callback can iterate over the items |
| in the list using the |
| [`length`](https://pub.dev/documentation/rfw/latest/rfw/DataSource/length.html) |
| method, which returns the length of the list (or zero if the key does |
| not identify a list): |
| |
| <?code-excerpt "test/readme_test.dart (length)"?> |
| ```dart |
| 'Foo': (BuildContext context, DataSource source) { |
| final int length = source.length(<Object>['text']); |
| if (length > 0) { |
| final StringBuffer text = StringBuffer(); |
| for (int index = 0; index < length; index += 1) { |
| text.write(source.v<String>(<Object>['text', index])); |
| } |
| return Text(text.toString(), textDirection: TextDirection.ltr); |
| } |
| return const Text('<empty>', textDirection: TextDirection.ltr); |
| }, |
| ``` |
| |
| This could be used like this: |
| |
| <?code-excerpt "test/readme_test.dart (example3)"?> |
| ```rfwtxt |
| widget example3 = GreenBox( |
| child: Foo( |
| text: ['apple', 'banana'] |
| ), |
| ); |
| ``` |
| |
| #### Widgets |
| |
| The `GreenBox` widget has a child widget, which is itself specified by |
| the remote widget. This is common, for example, `Row` and `Column` |
| widgets have children, `Center` has a child, and so on. Indeed, most |
| widgets have children, except for those like `Text`, `Image`, and |
| `Spacer`. |
| |
| The `GreenBox` definition uses |
| [`DataSource.child`](https://pub.dev/documentation/rfw/latest/rfw/DataSource/child.html) |
| to obtain the widget, in a manner similar to the `v` method: |
| |
| <?code-excerpt "test/readme_test.dart (child)"?> |
| ```rfwtxt |
| 'GreenBox': (BuildContext context, DataSource source) { |
| return Container(color: const Color(0xFF002211), child: source.child(<Object>['child'])); |
| }, |
| ``` |
| |
| Rather than returning `null` when the specified key points to an |
| argument that isn't a widget, the `child` method returns an |
| `ErrorWidget`. For cases where having `null` is acceptable, the |
| [`optionalChild`](https://pub.dev/documentation/rfw/latest/rfw/DataSource/optionalChild.html) method can be used: |
| |
| <?code-excerpt "test/readme_test.dart (optionalChild)"?> |
| ```rfwtxt |
| 'GreenBox': (BuildContext context, DataSource source) { |
| return Container(color: const Color(0xFF002211), child: source.optionalChild(<Object>['child'])); |
| }, |
| ``` |
| |
| It returns `null` when the specified key does not point to a widget. |
| |
| For widgets that take lists of children, the |
| [`childList`](https://pub.dev/documentation/rfw/latest/rfw/DataSource/childList.html) |
| method can be used. For example, this is how `Row` is defined in |
| `createCoreWidgets` (see in particular the `children` line): |
| |
| <?code-excerpt "lib/src/flutter/core_widgets.dart (Row)"?> |
| ```rfwtxt |
| 'Row': (BuildContext context, DataSource source) { |
| return Row( |
| mainAxisAlignment: ArgumentDecoders.enumValue<MainAxisAlignment>(MainAxisAlignment.values, source, ['mainAxisAlignment']) ?? MainAxisAlignment.start, |
| mainAxisSize: ArgumentDecoders.enumValue<MainAxisSize>(MainAxisSize.values, source, ['mainAxisSize']) ?? MainAxisSize.max, |
| crossAxisAlignment: ArgumentDecoders.enumValue<CrossAxisAlignment>(CrossAxisAlignment.values, source, ['crossAxisAlignment']) ?? CrossAxisAlignment.center, |
| textDirection: ArgumentDecoders.enumValue<TextDirection>(TextDirection.values, source, ['textDirection']), |
| verticalDirection: ArgumentDecoders.enumValue<VerticalDirection>(VerticalDirection.values, source, ['verticalDirection']) ?? VerticalDirection.down, |
| textBaseline: ArgumentDecoders.enumValue<TextBaseline>(TextBaseline.values, source, ['textBaseline']), |
| children: source.childList(['children']), |
| ); |
| }, |
| ``` |
| |
| #### `ArgumentDecoders` |
| |
| It is common to need to decode types that are more structured than |
| merely `int`, `double`, `bool`, or `String` scalars, for example, |
| enums, `Color`s, or `Paint`s. |
| |
| The |
| [`ArgumentDecoders`](https://pub.dev/documentation/rfw/latest/rfw/ArgumentDecoders-class.html) |
| namespace offers some utility functions to make the decoding of such |
| values consistent. |
| |
| For example, the `Row` definition above has some cases of enums. To |
| decode them, it uses the |
| [`ArgumentDecoders.enumValue`](https://pub.dev/documentation/rfw/latest/rfw/ArgumentDecoders/enumValue.html) |
| method. |
| |
| #### Handlers |
| |
| The last kind of argument that widgets can have is callbacks. |
| |
| Since remote widget libraries are declarative and not code, they |
| cannot represent executable closures. Instead, they are represented as |
| events. For example, here is how the "7" button from the |
| [calculator example](https://github.com/flutter/packages/blob/main/packages/rfw/example/wasm/logic/calculator.rfwtxt) |
| is represented: |
| |
| <?code-excerpt "example/wasm/logic/calculator.rfwtxt (button7)"?> |
| ```rfwtxt |
| CalculatorButton(label: "7", onPressed: event "digit" { arguments: [7] }), |
| ``` |
| |
| This creates a `CalculatorButton` widget with two arguments, `label`, |
| a string, and `onPressed`, an event, whose name is "digit" and whose |
| arguments are a map with one key, "arguments", whose value is a list |
| with one value 7. |
| |
| In that example, `CalculatorButton` is itself a remote widget that is |
| defined in terms of a `Button`, and the `onPressed` argument |
| is passed to the `onPressed` of the `Button`, like this: |
| |
| <?code-excerpt "example/wasm/logic/calculator.rfwtxt (CalculatorButton)"?> |
| ```rfwtxt |
| widget CalculatorButton = Padding( |
| padding: [8.0], |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| child: Button( |
| child: FittedBox(child: Text(text: args.label)), |
| onPressed: args.onPressed, |
| ), |
| ), |
| ); |
| ``` |
| |
| Subsequently, `Button` is defined in terms of a `GestureDetector` |
| local widget (which is defined in terms of the `GestureDetector` |
| widget from the Flutter framework), and the `args.onPressed` is passed |
| to the `onTap` argument of that `GestureDetector` local widget (and |
| from there subsequently to the Flutter framework widget). |
| |
| When all is said and done, and the button is pressed, an event with |
| the name "digit" and the given arguments is reported to the |
| `RemoteWidget`'s `onEvent` callback. That callback takes two |
| arguments, the event name and the event arguments. |
| |
| On the implementation side, in local widget libraries, arguments like |
| the `onTap` of the `GestureDetector` local widget must be turned into |
| a Dart closure that is passed to the actual Flutter widget called |
| `GestureDetector` as the value of its `onTap` callback. |
| |
| The simplest kind of callback closure is a `VoidCallback` (no |
| arguments, no return value). To turn an `event` value in a local |
| widget's arguments in the local widget library into a `VoidCallback` |
| in Dart that reports the event as described above, the |
| `DataSource.voidHandler` method is used. For example, here is a |
| simplified `GestureDetector` local widget that just implements `onTap` |
| (when implementing similar local widgets, you may use a similar |
| technique): |
| |
| <?code-excerpt "test/readme_test.dart (onTap)"?> |
| ```dart |
| return <WidgetLibrary>[ |
| LocalWidgetLibrary(<String, LocalWidgetBuilder>{ |
| // The local widget is called `GestureDetector`... |
| 'GestureDetector': (BuildContext context, DataSource source) { |
| // The local widget is implemented using the `GestureDetector` |
| // widget from the Flutter framework. |
| return GestureDetector( |
| onTap: source.voidHandler(<Object>['onTap']), |
| // A full implementation of a `GestureDetector` local widget |
| // would have more arguments here, like `onTapDown`, etc. |
| child: source.optionalChild(<Object>['child']), |
| ); |
| }, |
| }), |
| ]; |
| ``` |
| |
| Sometimes, a callback has a different signature, in particular, it may |
| provide arguments. To convert the `event` value into a Dart callback |
| closure that reports an event as described above, the |
| `DataSource.handler` method is used. |
| |
| In addition to the list of keys that identify the `event` value, the |
| method itself takes a callback closure. That callback's purpose is to |
| convert the given `trigger` (a function which, when called, reports |
| the event) into the kind of callback closure the `Widget` expects. |
| This is usually written something like the following: |
| |
| <?code-excerpt "test/readme_test.dart (onTapDown)"?> |
| ```dart |
| return GestureDetector( |
| onTapDown: source.handler(<Object>['onTapDown'], (HandlerTrigger trigger) => (TapDownDetails details) => trigger()), |
| child: source.optionalChild(<Object>['child']), |
| ); |
| ``` |
| |
| To break this down more clearly: |
| |
| <?code-excerpt "test/readme_test.dart (onTapDown-long)"?> |
| ```dart |
| return GestureDetector( |
| // onTapDown expects a function that takes a TapDownDetails |
| onTapDown: source.handler<GestureTapDownCallback>( // this returns a function that takes a TapDownDetails |
| <Object>['onTapDown'], |
| (HandlerTrigger trigger) { // "trigger" is the function that will send the event to RemoteWidget.onEvent |
| return (TapDownDetails details) { // this is the function that is returned by handler() above |
| trigger(); // the function calls "trigger" |
| }; |
| }, |
| ), |
| child: source.optionalChild(<Object>['child']), |
| ); |
| ``` |
| |
| In some cases, the arguments sent to the callback (the |
| `TapDownDetails` in this case) are useful and should be passed to the |
| `RemoteWidget.onEvent` as part of its arguments. This can be done by |
| passing some values to the `trigger` method, as in: |
| |
| <?code-excerpt "test/readme_test.dart (onTapDown-position)"?> |
| ```dart |
| return GestureDetector( |
| onTapDown: source.handler(<Object>['onTapDown'], (HandlerTrigger trigger) { |
| return (TapDownDetails details) => trigger(<String, Object>{ |
| 'x': details.globalPosition.dx, |
| 'y': details.globalPosition.dy, |
| }); |
| }), |
| child: source.optionalChild(<Object>['child']), |
| ); |
| ``` |
| |
| Any arguments in the `event` get merged with the arguments passed to |
| the trigger. |
| |
| #### Animations |
| |
| The `rfw` package introduces a new Flutter widget called |
| [`AnimationDefaults`](https://pub.dev/documentation/rfw/latest/rfw/AnimationDefaults-class.html). |
| |
| This widget is exposed by `createCoreWidgets` under the same name, and |
| can be exposed in other local widget libraries as desired. This allows |
| remote widget libraries to configure the animation speed and curves of |
| entire subtrees more conveniently than repeating the details for each |
| widget. |
| |
| To support this widget, implement curve arguments using |
| [`ArgumentDecoders.curve`](https://pub.dev/documentation/rfw/latest/rfw/ArgumentDecoders/curve.html) |
| and duration arguments using |
| [`ArgumentDecoders.duration`](https://pub.dev/documentation/rfw/latest/rfw/ArgumentDecoders/duration.html). |
| This automatically defers to the defaults provided by |
| `AnimationDefaults`. Alternatively, the |
| [`AnimationDefaults.curveOf`](https://pub.dev/documentation/rfw/latest/rfw/AnimationDefaults/curveOf.html) |
| and |
| [`AnimationDefaults.durationOf`](https://pub.dev/documentation/rfw/latest/rfw/AnimationDefaults/durationOf.html) |
| methods can be used with a `BuildContext` directly to get curve and |
| duration settings for animations. |
| |
| The settings default to 200ms and the |
| [`Curves.fastOutSlowIn`](https://api.flutter.dev/flutter/animation/Curves/fastOutSlowIn-constant.html) |
| curve. |
| |
| |
| ### Developing remote widget libraries |
| |
| Remote widget libraries are usually defined using a Remote Flutter |
| Widgets text library file (`rfwtxt` extension), which is then compiled |
| into a binary library file (`rfw` extension) on the server before |
| being sent to the client. |
| |
| The format of text library files is defined in detail in the API |
| documentation of the |
| [`parseLibraryFile`](https://pub.dev/documentation/rfw/latest/formats/parseLibraryFile.html) |
| function. |
| |
| Compiling a text `rfwtxt` file to the binary `rfw` format can be done |
| by calling |
| [`encodeLibraryBlob`](https://pub.dev/documentation/rfw/latest/formats/encodeLibraryBlob.html) |
| on the results of calling `parseLibraryFile`. |
| |
| The example in `example/wasm` has some [elaborate remote |
| widgets](https://github.com/flutter/packages/blob/main/packages/rfw/example/wasm/logic/calculator.rfwtxt), |
| including some that manipulate state (`Button`). |
| |
| #### State |
| |
| The canonical example of a state-manipulating widget is a button. |
| Buttons must react immediately (in milliseconds) and cannot wait for |
| logic that's possibly running on a remote server (maybe many hundreds |
| of milliseconds away). |
| |
| The aforementioned `Button` widget in the `wasm` example tracks a |
| local "down" state, manipulates it in reaction to |
| `onTapDown`/`onTapUp` events, and changes the shadow and margins of |
| the button based on its state: |
| |
| <?code-excerpt "example/wasm/logic/calculator.rfwtxt (Button)"?> |
| ```rfwtxt |
| widget Button { down: false } = GestureDetector( |
| onTap: args.onPressed, |
| onTapDown: set state.down = true, |
| onTapUp: set state.down = false, |
| onTapCancel: set state.down = false, |
| child: Container( |
| duration: 50, |
| margin: switch state.down { |
| false: [ 0.0, 0.0, 2.0, 2.0 ], |
| true: [ 2.0, 2.0, 0.0, 0.0 ], |
| }, |
| padding: [ 12.0, 8.0 ], |
| decoration: { |
| type: "shape", |
| shape: { |
| type: "stadium", |
| side: { width: 1.0 }, |
| }, |
| gradient: { |
| type: "linear", |
| begin: { x: -0.5, y: -0.25 }, |
| end: { x: 0.0, y: 0.5 }, |
| colors: [ 0xFFFFFF99, 0xFFEEDD00 ], |
| stops: [ 0.0, 1.0 ], |
| tileMode: "mirror", |
| }, |
| shadows: switch state.down { |
| false: [ { blurRadius: 4.0, spreadRadius: 0.5, offset: { x: 1.0, y: 1.0, } } ], |
| default: [], |
| }, |
| }, |
| child: DefaultTextStyle( |
| style: { |
| color: 0xFF000000, |
| fontSize: 32.0, |
| }, |
| child: args.child, |
| ), |
| ), |
| ); |
| ``` |
| |
| Because `Container` is implemented in `createCoreWidgets` using the |
| `AnimatedContainer` widget, changing the fields causes the button to |
| animate. The `duration: 50` argument sets the animation speed to 50ms. |
| |
| #### Lists |
| |
| Let us consider a remote widget library that is used to render data in |
| this form: |
| |
| <?code-excerpt "test/readme_test.dart (game-data)"?> |
| ```json |
| { "games": [ |
| {"rating": 8.219, "users-rated": 16860, "name": "Twilight Struggle", "rank": 1, "link": "/boardgame/12333/twilight-struggle", "id": 12333}, |
| {"rating": 8.093, "users-rated": 11750, "name": "Through the Ages: A Story of Civilization", "rank": 2, "link": "/boardgame/25613/through-ages-story-civilization", "id": 25613}, |
| {"rating": 8.088, "users-rated": 34745, "name": "Agricola", "rank": 3, "link": "/boardgame/31260/agricola", "id": 31260}, |
| {"rating": 8.082, "users-rated": 8913, "name": "Terra Mystica", "rank": 4, "link": "/boardgame/120677/terra-mystica", "id": 120677}, |
| // ยทยทยท |
| ``` |
| |
| For the sake of this example, let us assume this data is registered |
| with the `DynamicContent` under the name `server`. |
| |
| > This configuration data is both valid JSON and a valid RFW data file, |
| > which shows how similar the two syntaxes are. |
| > |
| > This data is parsed by calling |
| > [`parseDataFile`](https://pub.dev/documentation/rfw/latest/formats/parseDataFile.html), |
| > which turns it into |
| > [`DynamicMap`](https://pub.dev/documentation/rfw/latest/formats/DynamicMap.html). |
| > That object is then passed to a |
| > [`DynamicContent`](https://pub.dev/documentation/rfw/latest/rfw/DynamicContent-class.html), |
| > using |
| > [`DynamicContent.update`](https://pub.dev/documentation/rfw/latest/rfw/DynamicContent/update.html) |
| > (this is where the name `server` would be specified) which is passed |
| > to a |
| > [`RemoteWidget`](https://pub.dev/documentation/rfw/latest/rfw/RemoteWidget-class.html) |
| > via the |
| > [`data`](https://pub.dev/documentation/rfw/latest/rfw/RemoteWidget/data.html) |
| > property. |
| > |
| > Ideally, rather than dealing with this text form on the client, the |
| > data would be turned into a binary form using |
| > [`encodeDataBlob`](https://pub.dev/documentation/rfw/latest/formats/encodeDataBlob.html) |
| > on the server, and then parsed on the client using |
| > [`decodeDataBlob`](https://pub.dev/documentation/rfw/latest/formats/decodeDataBlob.html). |
| |
| First, let's render a plain Flutter `ListView` with the name of each |
| product. The `Shop` widget below achieves this: |
| |
| <?code-excerpt "test/readme_test.dart (Shop)"?> |
| ```rfwtxt |
| import core; |
| |
| widget Shop = ListView( |
| children: [ |
| Text(text: "Products:"), |
| ...for product in data.server.games: |
| Product(product: product) |
| ], |
| ); |
| |
| widget Product = Text(text: args.product.name, softWrap: false, overflow: "fade"); |
| ``` |
| |
| The `Product` widget here is not strictly necessary, it could be |
| inlined into the `Shop`. However, as with Flutter itself, it can be |
| easier to develop widgets when logically separate components are |
| separated into separate widgets. |
| |
| We can elaborate on this example, introducing a Material `AppBar`, |
| using a `ListTile` for the list items, and making them interactive (at |
| least in principle; the logic in the app would need to know how to |
| handle the "shop.productSelect" event): |
| |
| <?code-excerpt "test/readme_test.dart (MaterialShop)"?> |
| ```rfwtxt |
| import core; |
| import material; |
| |
| widget MaterialShop = Scaffold( |
| appBar: AppBar( |
| title: Text(text: ['Products']), |
| ), |
| body: ListView( |
| children: [ |
| ...for product in data.server.games: |
| Product(product: product) |
| ], |
| ), |
| ); |
| |
| widget Product = ListTile( |
| title: Text(text: args.product.name), |
| onTap: event 'shop.productSelect' { name: args.product.name, path: args.product.link }, |
| ); |
| ``` |
| |
| ### Fetching remote widget libraries remotely |
| |
| The example in `example/remote` shows how a program could fetch |
| different user interfaces at runtime. In this example, the interface |
| used on startup is the one last cached locally. Each time the program |
| is run, after displaying the currently-cached interface, the |
| application fetches a new interface over the network, overwriting the |
| one in the cache, so that a different interface is used the next time |
| the app is run. |
| |
| This example also shows how an application can implement custom local |
| code for events; in this case, incrementing a counter (both of the |
| "remote" widgets are just different ways of implementing a counter). |
| |
| ### Integrating with scripting language runtimes |
| |
| The example in `example/wasm` shows how a program could fetch logic in |
| addition to UI, in this case using Wasm compiled from C (and let us |
| briefly appreciate the absurdity of using C as a scripting language |
| for an application written in Dart). |
| |
| In this example, as written, the Dart client could support any |
| application whose data model consisted of a single integer and whose |
| logic could be expressed in C without external dependencies. |
| |
| This example could be extended to have the C program export data in |
| the Remote Flutter Widgets binary data blob format which could be |
| parsed using `decodeDataBlob` and passed to `DynamicContent.update` |
| (thus allowing any structured data supported by RFW), and similarly |
| arguments could be passed to the Wasm code using the same format |
| (encoding using `encodeDataBlob`) to allow arbitrary structured data |
| to be communicated from the interface to the Wasm logic. In addition, |
| the Wasm logic could be provided with WASI interface bindings or with |
| custom bindings that expose platform capabilities (e.g. from Flutter |
| plugins), greatly extending the scope of what could be implemented in |
| the Wasm logic. |
| |
| As of the time of writing, `package:wasm` does not support Android, |
| iOS, or web, so this demo is limited to desktop environments. The |
| underlying Wasmer runtime supports Android and iOS already, and |
| obviously Wasm in general is supported by web browsers, so it is |
| expected that these limitations are only temporary (modulo policy |
| concerns on iOS, anyway). |
| |
| ## Contributing |
| |
| See [CONTRIBUTING.md](https://github.com/flutter/packages/blob/main/packages/rfw/CONTRIBUTING.md) |