blob: a0e594d3a687e7fbb2184c19154c6f2a5967d8b3 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file is hand-formatted.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import '../dart/model.dart';
import 'content.dart';
/// Signature of builders for local widgets.
///
/// The [LocalWidgetLibrary] class wraps a map of widget names to
/// [LocalWidgetBuilder] callbacks.
typedef LocalWidgetBuilder = Widget Function(BuildContext context, DataSource source);
/// Signature of the callback passed to a [RemoteWidget].
///
/// This is used by [RemoteWidget] and [Runtime.build] as the callback for
/// events triggered by remote widgets.
typedef RemoteEventHandler = void Function(String eventName, DynamicMap eventArguments);
/// Signature of the callback passed to [DataSource.handler].
///
/// The callback should return a function of type `T`. That function should call
/// `trigger`.
///
/// See [DataSource.handler] for details.
typedef HandlerGenerator<T extends Function> = T Function(HandlerTrigger trigger);
/// Signature of the callback passed to a [HandlerGenerator].
///
/// See [DataSource.handler] for details.
typedef HandlerTrigger = void Function([DynamicMap? extraArguments]);
/// Used to indicate that there is an error with one of the libraries loaded
/// into the Remote Flutter Widgets [Runtime].
///
/// For example, a reference to a state variable did not match any actual state
/// values, or a library import loop.
class RemoteFlutterWidgetsException implements Exception {
/// Creates a [RemoteFlutterWidgetsException].
///
/// The message should be a complete sentence, starting with a capital letter
/// and ending with a period.
const RemoteFlutterWidgetsException(this.message);
/// A description of the problem that was detected.
///
/// This will end with a period.
final String message;
@override
String toString() => message;
}
/// Interface for [LocalWidgetBuilder] to obtain data from arguments.
///
/// The interface exposes the [v] method, the argument to which is a list of
/// keys forming a path to a node in the arguments expected by the widget. If
/// the method's type argument does not match the value obtained, null is
/// returned instead.
///
/// In addition, to fetch widgets specifically, the [child] and [childList]
/// methods must be used, and to fetch event handlers, the [handler] method must
/// be used.
///
/// The [isList] and [isMap] methods can be used to avoid inspecting keys that
/// may not be present (e.g. before reading 15 keys in a map that isn't even
/// present, consider checking if the map is present using [isMap] and
/// short-circuiting the key lookups if it is not).
///
/// To iterate over a list, the [length] method can be used to find the number
/// of items in the list.
abstract class DataSource {
/// Return the int, double, bool, or String value at the given path of the
/// arguments to the widget.
///
/// If `T` is not [Object] and `T` does not match the type of the value
/// obtained, then the method returns null.
T? v<T>(List<Object> argsKey);
/// Return true if the given key identifies a list, otherwise false.
bool isList(List<Object> argsKey);
/// Return the length of the list at the given path of the arguments to the
/// widget.
///
/// If the given path does not identify a list, returns zero.
int length(List<Object> argsKey);
/// Return true if the given key identifies a map, otherwise false.
bool isMap(List<Object> argsKey);
/// Build the child at the given key.
///
/// If the node specified is not a widget, returns an [ErrorWidget].
///
/// See also:
///
/// * [optionalChild], which returns null if the widget is missing.
Widget child(List<Object> argsKey);
/// Build the child at the given key.
///
/// If the node specified is not a widget, returns null.
///
/// See also:
///
/// * [child], which returns an [ErrorWidget] instead of null if the widget
/// is missing.
Widget? optionalChild(List<Object> argsKey);
/// Builds the children at the given key.
///
/// If the node is missing, returns an empty list.
///
/// If the node specified is not a list of widgets, returns a list with the
/// non-widget nodes replaced by [ErrorWidget].
List<Widget> childList(List<Object> argsKey);
/// Gets a [VoidCallback] event handler at the given key.
///
/// If the node specified is an [AnyEventHandler] or a [DynamicList] of
/// [AnyEventHandler]s, returns a callback that invokes the specified event
/// handler(s), merging the given `extraArguments` into the arguments
/// specified in each event handler. In the event of a key conflict (where
/// both the arguments specified in the remote widget declaration and the
/// argument provided to this method have the same name), the arguments
/// specified here take precedence.
VoidCallback? voidHandler(List<Object> argsKey, [ DynamicMap? extraArguments ]);
/// Gets an event handler at the given key.
///
/// The event handler can be of any Function type, as specified by the type
/// argument `T`.
///
/// When this method is called, the second argument, `generator`, is invoked.
/// The `generator` callback must return a function, which we will call
/// _entrypoint_, that matches the signature of `T`. The `generator` callback
/// receives an argument, which we will call `trigger`. The _entrypoint_
/// function must call `trigger`, optionally passing it any extra arguments
/// that should be merged into the arguments specified in each event handler.
///
/// This is admittedly a little confusing. At its core, the problem is that
/// this method cannot itself automatically create a function (_entrypoint_)
/// of the right type (`T`), and therefore a callback (`generator`) that knows
/// how to wrap a function body (`trigger`) in the right signature (`T`) is
/// needed to actually build that function (_entrypoint_).
T? handler<T extends Function>(List<Object> argsKey, HandlerGenerator<T> generator);
}
/// Widgets defined by the client application. All remote widgets eventually
/// bottom out in these widgets.
class LocalWidgetLibrary extends WidgetLibrary {
/// Create a [LocalWidgetLibrary].
///
/// The given map must not change once the object is created.
LocalWidgetLibrary(this._widgets);
final Map<String, LocalWidgetBuilder> _widgets;
/// Returns the builder for the widget of the given name, if any.
@protected
LocalWidgetBuilder? findConstructor(String name) {
return _widgets[name];
}
}
class _ResolvedConstructor {
const _ResolvedConstructor(this.fullName, this.constructor);
final FullyQualifiedWidgetName fullName;
final Object constructor;
}
/// The logic that builds and maintains Remote Flutter Widgets.
///
/// To declare the libraries of widgets, the [update] method is used.
///
/// At least one [LocalWidgetLibrary] instance must be declared
/// so that [RemoteWidgetLibrary] instances can resolve to real widgets.
///
/// The [build] method returns a [Widget] generated from one of the libraries of
/// widgets added in this manner. Generally, it is simpler to use the
/// [RemoteWidget] widget (which calls [build]).
class Runtime extends ChangeNotifier {
/// Create a [Runtime] object.
///
/// This object should be [dispose]d when it is no longer needed.
Runtime();
final Map<LibraryName, WidgetLibrary> _libraries = <LibraryName, WidgetLibrary>{};
/// Replace the definitions of the specified library (`name`).
///
/// References to widgets that are not defined in the available libraries will
/// default to using the [ErrorWidget] widget.
///
/// [LocalWidgetLibrary] and [RemoteWidgetLibrary] instances are added using
/// this method.
///
/// [RemoteWidgetLibrary] instances are typically first obtained using
/// [decodeLibraryBlob].
///
/// To remove a library, the libraries must be cleared using [clearLibraries]
/// and then all the libraries must be readded.
void update(LibraryName name, WidgetLibrary library) {
_libraries[name] = library;
_clearCache();
}
/// Remove all the libraries and start afresh.
///
/// Calling this notifies the listeners, which typically causes them to
/// rebuild their widgets in the next frame (for example, that is how
/// [RemoteWidget] is implemented). If no libraries are readded after calling
/// [clearLibraries], and there are any listeners, they will fail to rebuild
/// any widgets that they were configured to create. For this reason, this
/// call should usually be immediately followed by calls to [update].
void clearLibraries() {
_libraries.clear();
_clearCache();
}
final Map<FullyQualifiedWidgetName, _ResolvedConstructor?> _cachedConstructors = <FullyQualifiedWidgetName, _ResolvedConstructor?>{};
final Map<FullyQualifiedWidgetName, _CurriedWidget> _widgets = <FullyQualifiedWidgetName, _CurriedWidget>{};
void _clearCache() {
_cachedConstructors.clear();
_widgets.clear();
notifyListeners();
}
/// Build the root widget of a Remote Widget subtree.
///
/// The widget is identified by a [FullyQualifiedWidgetName], which identifies
/// a library and a widget name. The widget does not strictly have to be in
/// that library, so long as it is in that library's dependencies.
///
/// The data for the widget is given by the `data` argument. That object can
/// be updated independently, the widget will rebuild appropriately as it
/// changes.
///
/// The `remoteEventTarget` argument is the callback to invoke whenever a
/// remote widget event handler is triggered.
Widget build(BuildContext context, FullyQualifiedWidgetName widget, DynamicContent data, RemoteEventHandler remoteEventTarget) {
final _CurriedWidget boundWidget;
if (_widgets.containsKey(widget)) {
boundWidget = _widgets[widget]!;
} else {
_checkForImportLoops(widget.library);
boundWidget = _applyConstructorAndBindArguments(widget, const <String, Object?>{}, -1, <FullyQualifiedWidgetName>{});
_widgets[widget] = boundWidget;
}
return boundWidget.build(context, data, remoteEventTarget, const <_WidgetState>[]);
}
void _checkForImportLoops(LibraryName name, [ Set<LibraryName>? visited ]) {
final WidgetLibrary? library = _libraries[name];
if (library is RemoteWidgetLibrary) {
visited ??= <LibraryName>{};
visited.add(name);
for (final Import import in library.imports) {
final LibraryName dependency = import.name;
if (visited.contains(dependency)) {
final List<LibraryName> path = <LibraryName>[dependency];
for (final LibraryName name in visited.toList().reversed) {
if (name == dependency) {
break;
}
path.add(name);
}
if (path.length == 1) {
assert(path.single == dependency);
throw RemoteFlutterWidgetsException('Library $dependency depends on itself.');
} else {
throw RemoteFlutterWidgetsException('Library $dependency indirectly depends on itself via ${path.reversed.join(" which depends on ")}.');
}
}
_checkForImportLoops(dependency, visited.toSet());
}
}
}
_ResolvedConstructor? _findConstructor(FullyQualifiedWidgetName fullName) {
if (_cachedConstructors.containsKey(fullName)) {
return _cachedConstructors[fullName];
}
final WidgetLibrary? library = _libraries[fullName.library];
if (library is RemoteWidgetLibrary) {
for (final WidgetDeclaration constructor in library.widgets) {
if (constructor.name == fullName.widget) {
return _cachedConstructors[fullName] = _ResolvedConstructor(fullName, constructor);
}
}
for (final Import import in library.imports) {
final LibraryName dependency = import.name;
final _ResolvedConstructor? result = _findConstructor(FullyQualifiedWidgetName(dependency, fullName.widget));
if (result != null) {
return _cachedConstructors[fullName] = result;
}
}
} else if (library is LocalWidgetLibrary) {
final LocalWidgetBuilder? constructor = library.findConstructor(fullName.widget);
if (constructor != null) {
return _cachedConstructors[fullName] = _ResolvedConstructor(fullName, constructor);
}
} else {
assert(library is Null); // ignore: prefer_void_to_null, type_check_with_null, https://github.com/dart-lang/sdk/issues/47017#issuecomment-907562014
}
_cachedConstructors[fullName] = null;
return null;
}
Iterable<LibraryName> _findMissingLibraries(LibraryName library) sync* {
final WidgetLibrary? root = _libraries[library];
if (root == null) {
yield library;
return;
}
if (root is LocalWidgetLibrary) {
return;
}
root as RemoteWidgetLibrary;
for (final Import import in root.imports) {
yield* _findMissingLibraries(import.name);
}
}
/// Resolves argument references ([ArgsReference] objects) in the given
/// `node`, and applies [ConstructorCall]s so that all remaining widgets are
/// local widgets.
_CurriedWidget _applyConstructorAndBindArguments(FullyQualifiedWidgetName fullName, DynamicMap arguments, int stateDepth, Set<FullyQualifiedWidgetName> usedWidgets) {
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.');
}
usedWidgets = usedWidgets.toSet()..add(widget.fullName);
final WidgetDeclaration constructor = widget.constructor as WidgetDeclaration;
int newDepth;
if (constructor.initialState != null) {
newDepth = stateDepth + 1;
} else {
newDepth = stateDepth;
}
Object result = _bindArguments(widget.fullName, constructor.root, arguments, newDepth, usedWidgets);
if (result is Switch) {
result = _CurriedSwitch(widget.fullName, result, arguments, constructor.initialState);
} else {
result as _CurriedWidget;
if (constructor.initialState != null) {
result = _CurriedRemoteWidget(widget.fullName, result, arguments, constructor.initialState);
}
}
return result as _CurriedWidget;
}
assert(widget.constructor is LocalWidgetBuilder);
return _CurriedLocalWidget(widget.fullName, widget.constructor as LocalWidgetBuilder, arguments);
}
final Set<LibraryName> missingLibraries = _findMissingLibraries(fullName.library).toSet();
if (missingLibraries.isNotEmpty) {
return _CurriedLocalWidget.error(
fullName,
'Could not find remote widget named ${fullName.widget} in ${fullName.library}, '
'possibly because some dependencies were missing: ${missingLibraries.join(", ")}',
);
}
return _CurriedLocalWidget.error(fullName, 'Could not find remote widget named ${fullName.widget} in ${fullName.library}.');
}
Object _bindArguments(FullyQualifiedWidgetName context, Object node, Object arguments, 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);
}
if (node is DynamicMap) {
return node.map<String, Object?>(
(String name, Object? value) => MapEntry<String, Object?>(name, _bindArguments(context, value!, arguments, stateDepth, usedWidgets)),
);
}
if (node is DynamicList) {
return node.map<Object>(
(Object? value) => _bindArguments(context, value!, arguments, stateDepth, usedWidgets),
).toList();
}
if (node is Loop) {
final Object input = _bindArguments(context, node.input, arguments, stateDepth, usedWidgets);
final Object output = _bindArguments(context, node.output, arguments, stateDepth, usedWidgets);
return Loop(input, output);
}
if (node is Switch) {
return Switch(
_bindArguments(context, node.input, arguments, 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),
);
},
),
);
}
if (node is ArgsReference) {
return node.bind(arguments);
}
if (node is StateReference) {
return node.bind(stateDepth);
}
if (node is EventHandler) {
return EventHandler(node.eventName, _bindArguments(context, node.eventArguments, arguments, stateDepth, usedWidgets) as DynamicMap);
}
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));
}
assert(node is! WidgetDeclaration);
return node;
}
}
// Internal structure to represent the result of indexing into a list.
//
// There are two ways this can go: either we index in and find a result, in
// which case [result] is that value and the other fields are null, or we fail
// to index into the list and we obtain the length as a side-effect, in which
// case [result] is null, [rawList] is the raw list (might contain [Loop] objects),
// and [length] is the effective length after expanding all the internal loops.
class _ResolvedDynamicList {
const _ResolvedDynamicList(this.rawList, this.result, this.length);
final DynamicList? rawList;
final Object? result; // null means out of range
final int? length; // might be null if result is not null
}
typedef _DataResolverCallback = Object Function(List<Object> dataKey);
typedef _StateResolverCallback = Object Function(List<Object> stateKey, int depth);
abstract class _CurriedWidget extends BlobNode {
const _CurriedWidget(this.fullName, this.arguments, this.initialState);
final FullyQualifiedWidgetName fullName;
final DynamicMap arguments;
final DynamicMap? initialState;
static Object _bindLoopVariable(Object node, Object argument, int depth) {
if (node is DynamicMap) {
return node.map<String, Object?>(
(String name, Object? value) => MapEntry<String, Object?>(name, _bindLoopVariable(value!, argument, depth)),
);
}
if (node is DynamicList) {
return node.map<Object>(
(Object? value) => _bindLoopVariable(value!, argument, depth),
).toList();
}
if (node is Loop) {
return Loop(_bindLoopVariable(node.input, argument, depth), _bindLoopVariable(node.output, argument, depth + 1));
}
if (node is Switch) {
return Switch(
_bindLoopVariable(node.input, argument, depth),
node.outputs.map<Object?, Object>(
(Object? key, Object value) => MapEntry<Object?, Object>(
key == null ? null : _bindLoopVariable(key, argument, depth),
_bindLoopVariable(value, argument, depth),
),
)
);
}
if (node is _CurriedLocalWidget) {
return _CurriedLocalWidget(
node.fullName,
node.child,
_bindLoopVariable(node.arguments, argument, depth) as DynamicMap,
);
}
if (node is _CurriedRemoteWidget) {
return _CurriedRemoteWidget(
node.fullName,
_bindLoopVariable(node.child, argument, depth) as _CurriedWidget,
_bindLoopVariable(node.arguments, argument, depth) as DynamicMap,
node.initialState,
);
}
if (node is _CurriedSwitch) {
return _CurriedSwitch(
node.fullName,
_bindLoopVariable(node.root, argument, depth) as Switch,
_bindLoopVariable(node.arguments, argument, depth) as DynamicMap,
node.initialState,
);
}
if (node is LoopReference) {
if (node.loop == depth) {
return node.bind(argument);
}
return node;
}
if (node is BoundArgsReference) {
return BoundArgsReference(_bindLoopVariable(node.arguments, argument, depth), node.parts);
}
if (node is EventHandler) {
return EventHandler(node.eventName, _bindLoopVariable(node.eventArguments, argument, depth) as DynamicMap);
}
if (node is SetStateHandler) {
return SetStateHandler(node.stateReference, _bindLoopVariable(node.value, argument, depth));
}
return node;
}
/// Look up the _index_th entry in `list`, expanding any loops in `list`.
///
/// If `index` is -1, this evaluates the entire list to ensure the length is available.
//
// 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) {
if (list.any((Object? entry) => entry is Loop)) {
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) {
final Object node = list[currentIndex]!;
if (node is Loop) {
Object inputList = node.input;
while (inputList is! DynamicList) {
if (inputList is BoundArgsReference) {
inputList = _resolveFrom(inputList.arguments, inputList.parts, stateResolver, dataResolver);
} 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);
} else if (inputList is Switch) {
inputList = _resolveFrom(inputList, const <Object>[], stateResolver, dataResolver);
} 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);
if (entry.result != null) {
final Object boundResult = _bindLoopVariable(node.output, entry.result!, 0);
return _ResolvedDynamicList(null, boundResult, null);
}
effectiveIndex += entry.length!;
} else { // list[currentIndex] is not a Loop
if (effectiveIndex == targetEffectiveIndex) {
return _ResolvedDynamicList(null, list[currentIndex], null);
}
effectiveIndex += 1;
}
currentIndex += 1;
}
return _ResolvedDynamicList(list, null, effectiveIndex);
} else {
if (targetEffectiveIndex < 0 || targetEffectiveIndex >= list.length) {
return _ResolvedDynamicList(list, null, list.length);
}
return _ResolvedDynamicList(list, list[targetEffectiveIndex]!, list.length);
}
}
static Object _resolveFrom(Object root, List<Object> parts, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
int index = 0;
Object current = root;
while (true) {
if (current is DataReference) {
if (index < parts.length) {
current = current.constructReference(parts.sublist(index));
index = parts.length;
}
current = dataResolver(current.parts);
continue;
} else if (current is BoundArgsReference) {
List<Object> nextParts = current.parts;
if (index < parts.length) {
nextParts += parts.sublist(index);
}
parts = nextParts;
current = current.arguments;
index = 0;
continue;
} else if (current is BoundStateReference) {
if (index < parts.length) {
current = current.constructReference(parts.sublist(index));
index = parts.length;
}
current = stateResolver(current.parts, current.depth);
continue;
} else if (current is BoundLoopReference) {
List<Object> nextParts = current.parts;
if (index < parts.length) {
nextParts += parts.sublist(index);
}
parts = nextParts;
current = current.value;
index = 0;
continue;
} else if (current is Switch) {
final Object key = _resolveFrom(current.input, const <Object>[], stateResolver, dataResolver);
Object? value = current.outputs[key];
if (value == null) {
value = current.outputs[null];
if (value == null) {
return missing;
}
}
current = value;
continue;
} else if (index >= parts.length) {
if (current is EventHandler) {
current = EventHandler(current.eventName, _fix(current.eventArguments, stateResolver, dataResolver) as DynamicMap);
} else if (current is SetStateHandler) {
current = SetStateHandler(current.stateReference, _fix(current.value, stateResolver, dataResolver));
}
break;
} else if (current is DynamicMap) {
if (parts[index] is! String) {
return missing;
}
if (!current.containsKey(parts[index])) {
return missing;
}
current = current[parts[index]]!;
} else if (current is DynamicList) {
if (parts[index] is! int) {
return missing;
}
current = _listLookup(current, parts[index] as int, stateResolver, dataResolver).result ?? missing;
} else {
assert(current is! ArgsReference);
assert(current is! StateReference);
assert(current is! LoopReference);
return missing;
}
index += 1;
}
assert(current is! Reference, 'Unexpected unbound reference (of type ${current.runtimeType}): $current');
assert(current is! Switch);
assert(current is! Loop);
return current;
}
static Object _fix(Object root, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
if (root is DynamicMap) {
return root.map((String key, Object? value) => MapEntry<String, Object?>(key, _fix(root[key]!, stateResolver, dataResolver)));
} 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));
} else {
return DynamicList.generate(root.length, (int index) => _fix(root[index]!, stateResolver, dataResolver));
}
} else if (root is BlobNode) {
return _resolveFrom(root, const <Object>[], stateResolver, dataResolver);
} else {
return root;
}
}
Object resolve(List<Object> parts, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver, { required bool expandLists }) {
Object result = _resolveFrom(arguments, parts, stateResolver, dataResolver);
if (result is DynamicList && expandLists) {
result = _listLookup(result, -1, stateResolver, dataResolver);
}
assert(result is! Reference);
assert(result is! Switch);
assert(result is! Loop);
return result;
}
Widget build(BuildContext context, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states) {
return _Widget(curriedWidget: this, data: data, remoteEventTarget: remoteEventTarget, states: states);
}
Widget buildChild(BuildContext context, DataSource source, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver);
@override
String toString() => '$fullName ${initialState ?? "{}"} $arguments';
}
class _CurriedLocalWidget extends _CurriedWidget {
const _CurriedLocalWidget(FullyQualifiedWidgetName fullName, this.child, DynamicMap arguments) : super(fullName, arguments, null);
factory _CurriedLocalWidget.error(FullyQualifiedWidgetName fullName, String message) {
return _CurriedLocalWidget(fullName, (BuildContext context, DataSource data) => _buildErrorWidget(message), const <String, Object?>{});
}
final LocalWidgetBuilder child;
@override
Widget buildChild(BuildContext context, DataSource source, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
return child(context, source);
}
}
class _CurriedRemoteWidget extends _CurriedWidget {
const _CurriedRemoteWidget(FullyQualifiedWidgetName fullName, this.child, DynamicMap arguments, DynamicMap? initialState) : super(fullName, arguments, initialState);
final _CurriedWidget child;
@override
Widget buildChild(BuildContext context, DataSource source, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
return child.build(context, data, remoteEventTarget, states);
}
@override
String toString() => '${super.toString()} = $child';
}
class _CurriedSwitch extends _CurriedWidget {
const _CurriedSwitch(FullyQualifiedWidgetName fullName, this.root, DynamicMap arguments, DynamicMap? initialState) : super(fullName, arguments, initialState);
final Switch root;
@override
Widget buildChild(BuildContext context, DataSource source, DynamicContent data, RemoteEventHandler remoteEventTarget, List<_WidgetState> states, _StateResolverCallback stateResolver, _DataResolverCallback dataResolver) {
Object result = _CurriedWidget._resolveFrom(root, const <Object>[], stateResolver, dataResolver);
if (result is _CurriedWidget) {
result = result.build(context, data, remoteEventTarget, states);
return result as Widget;
}
return _buildErrorWidget('Switch in $fullName did not resolve to a widget (got $result).');
}
@override
String toString() => '${super.toString()} = $root';
}
class _Widget extends StatefulWidget {
const _Widget({ Key? key, required this.curriedWidget, required this.data, required this.remoteEventTarget, required this.states }) : super(key: key);
final _CurriedWidget curriedWidget;
final DynamicContent data;
final RemoteEventHandler remoteEventTarget;
final List<_WidgetState> states;
@override
State<_Widget> createState() => _WidgetState();
}
class _WidgetState extends State<_Widget> implements DataSource {
DynamicContent? _state;
DynamicMap? _stateStore;
late List<_WidgetState> _states;
@override
void initState() {
super.initState();
_updateState();
}
@override
void didUpdateWidget(_Widget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.curriedWidget != widget.curriedWidget) {
_updateState();
}
if (oldWidget.data != widget.data || oldWidget.curriedWidget != widget.curriedWidget || oldWidget.states != widget.states) {
_unsubscribe();
}
}
@override
void dispose() {
_unsubscribe();
super.dispose();
}
void _updateState() {
_stateStore = deepClone(widget.curriedWidget.initialState) as DynamicMap?;
if (_stateStore != null) {
_state ??= DynamicContent();
_state!.updateAll(_stateStore!);
} else {
_state = null;
}
_states = widget.states;
if (_state != null) {
_states = _states.toList()..add(this);
}
}
void _handleSetState(int depth, List<Object> parts, Object value) {
_states[depth].applySetState(parts, value);
}
void applySetState(List<Object> parts, Object value) {
assert(parts.isNotEmpty);
assert(_stateStore != null);
int index = 0;
Object current = _stateStore!;
while (index < parts.length) {
final Object subindex = parts[index];
if (current is DynamicMap) {
if (subindex is! String) {
throw RemoteFlutterWidgetsException('${parts.join(".")} does not identify existing state.');
}
if (!current.containsKey(subindex)) {
throw RemoteFlutterWidgetsException('${parts.join(".")} does not identify existing state.');
}
if (index == parts.length - 1) {
current[subindex] = value;
} else {
current = current[parts[index]]!;
}
} else if (current is DynamicList) {
if (subindex is! int) {
throw RemoteFlutterWidgetsException('${parts.join(".")} does not identify existing state.');
}
if (subindex < 0 || subindex >= current.length) {
throw RemoteFlutterWidgetsException('${parts.join(".")} does not identify existing state.');
}
if (index == parts.length - 1) {
current[subindex] = value;
} else {
current = current[subindex]!;
}
} else {
throw RemoteFlutterWidgetsException('${parts.join(".")} does not identify existing state.');
}
index += 1;
}
_state!.updateAll(_stateStore!);
}
// List of subscriptions into [widget.data].
//
// Keys are into the [DynamicContent] object.
final Map<_Key, _Subscription> _subscriptions = <_Key, _Subscription>{};
void _unsubscribe() {
for (final _Subscription value in _subscriptions.values) {
value.dispose();
}
_subscriptions.clear();
_argsCache.clear();
}
@override
T? v<T>(List<Object> argsKey) {
assert(T == Object || T == int || T == double || T == bool || T == String);
final Object value = _fetch(argsKey, expandLists: false);
return value is T ? value as T : null;
}
@override
bool isList(List<Object> argsKey) {
final Object value = _fetch(argsKey, expandLists: false);
return value is _ResolvedDynamicList
|| value is DynamicList;
}
@override
int length(List<Object> argsKey) {
final Object value = _fetch(argsKey, expandLists: true);
if (value is _ResolvedDynamicList) {
if (value.rawList != null) {
assert(value.length != null);
return value.length!;
}
}
assert(value is! DynamicList);
return 0;
}
@override
bool isMap(List<Object> argsKey) {
final Object value = _fetch(argsKey, expandLists: false);
return value is DynamicMap;
}
@override
Widget child(List<Object> argsKey) {
final Object value = _fetch(argsKey, expandLists: false);
if (value is _CurriedWidget) {
return value.build(context, widget.data, widget.remoteEventTarget, widget.states);
}
return _buildErrorWidget('Not a widget at $argsKey (got $value) for ${widget.curriedWidget.fullName}.');
}
@override
Widget? optionalChild(List<Object> argsKey) {
final Object value = _fetch(argsKey, expandLists: false);
if (value is _CurriedWidget) {
return value.build(context, widget.data, widget.remoteEventTarget, widget.states);
}
return null;
}
@override
List<Widget> childList(List<Object> argsKey) {
final Object value = _fetch(argsKey, expandLists: true);
if (value is _ResolvedDynamicList) {
assert(value.length != null);
final DynamicList fullList = _fetchList(argsKey, value.length!);
return fullList.map<Widget>((Object? node) {
if (node is _CurriedWidget) {
return node.build(context, widget.data, widget.remoteEventTarget, _states);
}
return _buildErrorWidget('Not a widget at $argsKey (got $node) for ${widget.curriedWidget.fullName}.');
}).toList();
}
if (value == missing) {
return const <Widget>[];
}
return <Widget>[
_buildErrorWidget('Not a widget list at $argsKey (got $value) for ${widget.curriedWidget.fullName}.'),
];
}
@override
VoidCallback? voidHandler(List<Object> argsKey, [ DynamicMap? extraArguments ]) {
return handler<VoidCallback>(argsKey, (HandlerTrigger callback) => () => callback(extraArguments));
}
@override
T? handler<T extends Function>(List<Object> argsKey, HandlerGenerator<T> generator) {
Object value = _fetch(argsKey, expandLists: true);
if (value is AnyEventHandler) {
value = <Object>[ value ];
} else if (value is _ResolvedDynamicList) {
value = _fetchList(argsKey, value.length!);
}
if (value is DynamicList) {
final List<AnyEventHandler> handlers = value.whereType<AnyEventHandler>().toList();
if (handlers.isNotEmpty) {
return generator(([DynamicMap? extraArguments]) {
for (final AnyEventHandler entry in handlers) {
if (entry is EventHandler) {
DynamicMap arguments = entry.eventArguments;
if (extraArguments != null) {
arguments = DynamicMap.fromEntries(arguments.entries.followedBy(extraArguments.entries));
}
widget.remoteEventTarget(entry.eventName, arguments);
} else if (entry is SetStateHandler) {
assert(entry.stateReference is BoundStateReference);
_handleSetState((entry.stateReference as BoundStateReference).depth, entry.stateReference.parts, entry.value);
}
}
});
}
}
return null;
}
// null values means the data is not in the cache
final Map<_Key, Object?> _argsCache = <_Key, Object?>{};
bool _debugFetching = false;
final List<_Subscription> _dependencies = <_Subscription>[];
Object _fetch(List<Object> argsKey, { required bool expandLists }) {
final _Key key = _Key(_kArgsSection, argsKey);
final Object? value = _argsCache[key];
if (value != null && (value is! DynamicList || !expandLists)) {
return value;
}
assert(!_debugFetching);
try {
_debugFetching = true;
final Object result = widget.curriedWidget.resolve(argsKey, _stateResolver, _dataResolver, expandLists: expandLists);
for (final _Subscription subscription in _dependencies) {
subscription.addClient(key);
}
_argsCache[key] = result;
return result;
} finally {
_dependencies.clear();
_debugFetching = false;
}
}
DynamicList _fetchList(List<Object> argsKey, int length) {
return DynamicList.generate(length, (int index) {
return _fetch(<Object>[...argsKey, index], expandLists: false);
});
}
Object _dataResolver(List<Object> rawDataKey) {
final _Key dataKey = _Key(_kDataSection, rawDataKey);
final _Subscription subscription;
if (!_subscriptions.containsKey(dataKey)) {
subscription = _Subscription(widget.data, this, rawDataKey);
_subscriptions[dataKey] = subscription;
} else {
subscription = _subscriptions[dataKey]!;
}
_dependencies.add(subscription);
return subscription.value;
}
Object _stateResolver(List<Object> rawStateKey, int depth) {
final _Key stateKey = _Key(depth, rawStateKey);
final _Subscription subscription;
if (!_subscriptions.containsKey(stateKey)) {
if (depth >= _states.length) {
throw const RemoteFlutterWidgetsException('Reference to state value did not correspond to any stateful remote widget.');
}
final DynamicContent? state = _states[depth]._state;
if (state == null) {
return missing;
}
subscription = _Subscription(state, this, rawStateKey);
_subscriptions[stateKey] = subscription;
} else {
subscription = _subscriptions[stateKey]!;
}
_dependencies.add(subscription);
return subscription.value;
}
void updateData(Set<_Key> affectedArgs) {
setState(() {
for (final _Key key in affectedArgs) {
_argsCache[key] = null;
}
});
}
@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);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('name', '${widget.curriedWidget.fullName}'));
}
}
const int _kDataSection = -1;
const int _kArgsSection = -2;
@immutable
class _Key {
_Key(this.section, this.parts) : assert(_isValidKey(parts), '$parts is not a valid key');
static bool _isValidKey(List<Object> parts) {
return parts.every((Object segment) => segment is int || segment is String);
}
final int section;
final List<Object> parts;
@override
bool operator ==(Object other) {
return other is _Key // _Key has no subclasses, don't need to check runtimeType
&& section == other.section
&& listEquals(parts, other.parts);
}
@override
int get hashCode => hashValues(section, hashList(parts));
}
class _Subscription {
_Subscription(this._data, this._state, this._dataKey) {
_update(_data.subscribe(_dataKey, _update));
}
final DynamicContent _data;
final _WidgetState _state;
final List<Object> _dataKey;
final Set<_Key> _clients = <_Key>{};
Object get value => _value;
late Object _value;
void _update(Object value) {
_state.updateData(_clients);
_value = value;
}
void addClient(_Key key) {
_clients.add(key);
}
void dispose() {
_data.unsubscribe(_dataKey, _update);
}
}
ErrorWidget _buildErrorWidget(String message) {
FlutterError.reportError(FlutterErrorDetails(
exception: message,
stack: StackTrace.current,
library: 'Remote Flutter Widgets',
));
return ErrorWidget(message);
}