This package provides a mechanism for rendering widgets based on declarative UI descriptions that can be obtained at runtime.
This package was created without a clear idea of what problem it solves, in order to see if it was interesting to people using Flutter and to learn more about the problem space.
So far it has received only minimal feedback, which either means it's perfectly addressing the needs that people have from it or that it is completely out of touch with what people want. We would love to know which, so if you consider this package, please describe your experiences, positive or negative, on issue 90218. This will help us determine whether to spend more effort on this package, or whether we should look at creating other packages.
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).
A Flutter application can render remote widgets using the
RemoteWidget
widget, as in the following snippet:
// see example/hello
class Example extends StatefulWidget {
const Example({Key? key}) : super(key: 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"),
),
);
''');
@override
void initState() {
super.initState();
_runtime.update(const LibraryName(<String>['core', 'widgets']), createCoreWidgets());
_runtime.update(const LibraryName(<String>['main']), _remoteWidgets);
_data.update('greet', <String, Object>{ 'name': 'World' });
}
@override
Widget build(BuildContext context) {
return RemoteWidget(
runtime: _runtime,
data: _data,
widget: const FullyQualifiedWidgetName(LibraryName(<String>['main']), '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 hard-coded into the application.
In typical usage, the remote widgets come from a server at runtime,
either through HTTP or some other network transport. Separately, the
DynamicContent
data is updated, either from the server or based on
local data.
It is recommended that servers send binary data, decoded using
decodeLibraryBlob
and decodeDataBlob
, when providing updates for
the remote widget libraries and data.
Events (onEvent
) are signalled by the user's interactions with the
remote widgets. The client is responsible for handling them, either by
sending the data to the server for the server to update the data, or
directly, on the user's device.
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.
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] and [createMaterialWidgets]. 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.
// see example/local
class Example extends StatefulWidget {
const Example({Key? key}) : super(key: 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() {
super.reassemble();
_update();
}
static WidgetLibrary _createLocalWidgets() => 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));
},
});
void _update() {
_runtime.update(const LibraryName(<String>['local']), _createLocalWidgets());
_runtime.update(const LibraryName(<String>['remote']), parseLibraryFile('''
import local;
widget root = GreenBox(
child: Hello(name: "World"),
);
'''));
}
@override
Widget build(BuildContext context) {
return RemoteWidget(
runtime: _runtime,
data: _data,
widget: const FullyQualifiedWidgetName(LibraryName(<String>['remote']), 'root'),
onEvent: (String name, DynamicMap arguments) {
debugPrint('user triggered event "$name" with data: $arguments');
},
);
}
}
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).
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).
If you run into any problems, please file a new bug, though as noted above, you may have to fix the issue yourself and submit a PR. See our contributing guide for details.
Adding more widgets to lib/flutter/core_widgets.dart
and lib/flutter/material_widgets.dart
is welcome.
When contributing code, ensure that flutter test --coverage; lcov --list coverage/lcov.info
continues to show 100% test coverage, and
update test_coverage/bin/test_coverage.dart
with the appropriate
expectations to prevent future coverage regressions. (That program is
run by run_tests.sh
.)
Golden tests are only run against the Flutter master channel and only run on Linux, since minor rendering differences are expected on different platforms and on different versions of Flutter.