-
Notifications
You must be signed in to change notification settings - Fork 167
Node layer for a2ui_core + Flutter #1011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
494dff3
a82ac49
d19dd9b
54c7a41
725bff4
be8cbe9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,181 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Copyright 2025 The Flutter Authors. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use of this source code is governed by a BSD-style license that can be | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // found in the LICENSE file. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import 'package:logging/logging.dart'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import '../primitives/event_notifier.dart'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import '../primitives/reactivity.dart'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import 'resolved_binding.dart'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final _log = Logger('a2ui_core.nodes'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The `type` of a node whose component definition has not arrived yet. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const String placeholderType = 'Placeholder'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Resolved node properties, keyed by the component's schema property names. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typedef NodeProps = Map<String, Object?>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// One resolved component instance in the rendered tree. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// A node's [props] hold fully resolved values: a [ResolvedBinding] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// (snapshot plus optional write) for each dynamic value, ready-to-call | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// closures for actions, and live [ComponentNode] references (or lists of | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// them) for child properties. The tree is whatever is reachable from the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// resolver's root; there is no separate graph structure or traversal API. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Emission contract: [props] emits when this node's own resolved properties | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// change, including when a child *reference* is replaced (a placeholder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// upgrade, a deletion, a list change). It does not emit when a child's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// internal properties change; subscribe to the child's [props] for that. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class ComponentNode { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Identifier for this node in the rendered tree. The bare component id at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// the root data scope; for template-spawned items the scoped data path is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// appended (e.g. `item-card-[/items/0]`) so sibling keys are distinct. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Until the spec provides data-derived child keys (a2ui#1745), this id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// names a list position, not a data item: it is not stable across array | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// insertions or reorders. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String instanceId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The component id from the payload. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String componentId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The catalog component type, or `'Placeholder'`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String type; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The data model scope this node resolves against, e.g. `/items/0`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String dataPath; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final Signal<NodeProps> _props; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Resolved, reactive properties. Read without subscribing via `peek()`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ReadonlySignal<NodeProps> get props => _props; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final _onDestroyed = EventNotifier<void>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Fires exactly once, when this node is disposed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EventListenable<void> get onDestroyed => _onDestroyed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<void Function()> _cleanups = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool _disposed = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ComponentNode( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.instanceId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.componentId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.dataPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NodeProps initialProps, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : _props = signal(initialProps); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool get disposed => _disposed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool get isPlaceholder => type == placeholderType; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Registers teardown work to run when this node is disposed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void addCleanup(void Function() cleanup) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _cleanups.add(cleanup); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Replaces the resolved props, emitting only if a shallow comparison shows | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// a change. Callers must keep unchanged values reference-identical; the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// shallow comparison is exact only under that invariant. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void setProps(NodeProps next) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_disposed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final NodeProps previous = _props.peek(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!_shallowEqual(previous, next)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _props.value = next; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Tears down this node: runs registered cleanups, then fires | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// [onDestroyed]. Idempotent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void dispose() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_disposed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _disposed = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (final void Function() cleanup in _cleanups) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error, stackTrace) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A failing cleanup must not prevent the remaining ones from running. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _log.severe( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'ComponentNode cleanup error ($instanceId)', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stackTrace, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _cleanups = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+95
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Iterating directly over void dispose() {
if (_disposed) {
return;
}
_disposed = true;
final cleanups = List<void Function()>.of(_cleanups);
_cleanups.clear();
for (final void Function() cleanup in cleanups) {
try {
cleanup();
} catch (error, stackTrace) {
// A failing cleanup must not prevent the remaining ones from running.
_log.severe(
'ComponentNode cleanup error ($instanceId)',
error,
stackTrace,
);
}
}
} |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _onDestroyed.emit(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _onDestroyed.dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Serializes the resolved tree for debugging and headless assertions. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Child nodes serialize recursively, bindings serialize as their | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// snapshot value, and action closures serialize as the string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// `'<Action>'`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Map<String, Object?> toJson() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isPlaceholder) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {'id': componentId, 'type': placeholderType}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final serialized = <String, Object?>{'id': componentId, 'type': type}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (final MapEntry<String, Object?> entry in _props.peek().entries) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| serialized[entry.key] = _serializeValue(entry.value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return serialized; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Whether two prop values count as unchanged for the shallow comparison in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// [ComponentNode.setProps]: reference identity, with value equality for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// primitives (equal strings are not always [identical], so identity alone | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// would report equal-value updates as changes). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool sameValue(Object? a, Object? b) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (identical(a, b)) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a is String && b is String) return a == b; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a is num && b is num) return a == b; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a is bool && b is bool) return a == b; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a is ResolvedBinding && b is ResolvedBinding) return a == b; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object? _serializeValue(Object? value) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value is ComponentNode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value.toJson(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value is ResolvedBinding) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return _serializeValue(value.value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value is Function) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return '<Action>'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value is List) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value.map(_serializeValue).toList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value is Map) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <String, Object?>{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (final MapEntry<Object?, Object?> entry in value.entries) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry.key as String: _serializeValue(entry.value), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool _shallowEqual(NodeProps a, NodeProps b) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (identical(a, b)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a.length != b.length) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (final MapEntry<String, Object?> entry in a.entries) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!b.containsKey(entry.key) || !sameValue(entry.value, b[entry.key])) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+168
to
+181
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Iterating over
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
addCleanupis called on an already disposed node, it will append the cleanup function to_cleanupswhich will never be executed. We should guardaddCleanupto ignore or warn when called after disposal.