diff --git a/packages/rfw/CHANGELOG.md b/packages/rfw/CHANGELOG.md index 65db8bc2c480..412f7505d2d1 100644 --- a/packages/rfw/CHANGELOG.md +++ b/packages/rfw/CHANGELOG.md @@ -1,7 +1,11 @@ +## 1.0.21 + +* Adds support for subscribing to the root of a `DynamicContent` object. + ## 1.0.20 -* Adds OverflowBox material widget. -* Updates ButtonBar material widget implementation. +* Adds `OverflowBox` material widget. +* Updates `ButtonBar` material widget implementation. ## 1.0.19 diff --git a/packages/rfw/lib/src/flutter/content.dart b/packages/rfw/lib/src/flutter/content.dart index 84a1eab1ade1..4689eae3fa93 100644 --- a/packages/rfw/lib/src/flutter/content.dart +++ b/packages/rfw/lib/src/flutter/content.dart @@ -12,6 +12,9 @@ import 'package:flutter/foundation.dart' show objectRuntimeType; import '../dart/model.dart'; /// Signature for the callback passed to [DynamicContent.subscribe]. +/// +/// Do not modify the provided value (e.g. if it is a map or list). Doing so +/// would leave the [DynamicContent] in an inconsistent state. typedef SubscriptionCallback = void Function(Object value); /// Returns a copy of a data structure if it consists of only [DynamicMap]s, @@ -116,9 +119,12 @@ Object? deepClone(Object? template) { /// [missing] as the new value. It is not an error to subscribe to missing data. /// It _is_ an error to add [missing] values to the data model, however. /// +/// To subscribe to the root of the [DynamicContent], use an empty list as the +/// key when subscribing. +/// /// The [LocalWidgetBuilder]s passed to a [LocalWidgetLibrary] use a /// [DataSource] as their interface into the [DynamicContent]. To ensure the -/// integrity of the update mechanism, that interface only allows access to +/// integrity of the update mechanism, _that_ interface only allows access to /// leaves, not intermediate nodes (maps and lists). /// /// It is an error to subscribe to the same key multiple times with the same @@ -143,6 +149,13 @@ class DynamicContent { /// key. /// /// Existing keys that are not present in the given map are left unmodified. + /// + /// If the root node has subscribers (see [subscribe]), they are called once + /// per key in `initialData`, not just a single time. + /// + /// Collections (maps and lists) in `initialData` must not be mutated after + /// calling this method; doing so would leave the [DynamicContent] in an + /// inconsistent state. void updateAll(DynamicMap initialData) { for (final String key in initialData.keys) { final Object value = initialData[key] ?? missing; @@ -156,6 +169,10 @@ class DynamicContent { /// /// The `value` must consist exclusively of [DynamicMap], [DynamicList], [int], /// [double], [bool], and [String] objects. + /// + /// Collections (maps and lists) in `value` must not be mutated after calling + /// this method; doing so would leave the [DynamicContent] in an inconsistent + /// state. void update(String rootKey, Object value) { _root.updateKey(rootKey, deepClone(value)!); _scheduleCleanup(); @@ -167,7 +184,14 @@ class DynamicContent { /// The value is always non-null; if the value is missing, the [missing] /// object is used instead. /// + /// The empty key refers to the root of the [DynamicContent] object (i.e. + /// the map manipulated by [updateAll] and [update]). + /// /// Use [unsubscribe] when the subscription is no longer needed. + /// + /// Do not modify the value returned by this method or passed to the given + /// `callback` (e.g. if it is a map or list). Changes made in this manner will + /// leave the [DynamicContent] in an inconsistent state. Object subscribe(List key, SubscriptionCallback callback) { return _root.subscribe(key, 0, callback); } @@ -329,12 +353,6 @@ class _DynamicNode { _sendUpdates(value); } - void _sendUpdates(Object value) { - for (final SubscriptionCallback callback in _callbacks) { - callback(value); - } - } - void updateKey(String rootKey, Object value) { assert(_value is DynamicMap); assert(_hasValidType(value)); @@ -345,6 +363,13 @@ class _DynamicNode { if (_children.containsKey(rootKey)) { _children[rootKey]!.update(value); } + _sendUpdates(_value); + } + + void _sendUpdates(Object value) { + for (final SubscriptionCallback callback in _callbacks) { + callback(value); + } } @override diff --git a/packages/rfw/pubspec.yaml b/packages/rfw/pubspec.yaml index f2f8c72ab929..af77f3482000 100644 --- a/packages/rfw/pubspec.yaml +++ b/packages/rfw/pubspec.yaml @@ -2,7 +2,7 @@ name: rfw description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime." repository: https://github.com/flutter/packages/tree/main/packages/rfw issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+rfw%22 -version: 1.0.20 +version: 1.0.21 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/rfw/test/runtime_test.dart b/packages/rfw/test/runtime_test.dart index 476c472af953..89cb96d62d51 100644 --- a/packages/rfw/test/runtime_test.dart +++ b/packages/rfw/test/runtime_test.dart @@ -1074,4 +1074,18 @@ void main() { }); expect(tested, isTrue); }); + + testWidgets('DynamicContent subscriptions', (WidgetTester tester) async { + final List log = []; + final DynamicContent data = DynamicContent({ + 'a': [0, 1], + 'b': ['q', 'r'], + }); + data.subscribe([], (Object value) { log.add('root: $value'); }); + data.subscribe(['a', 0], (Object value) { log.add('leaf: $value'); }); + data.update('a', [2, 3]); + expect(log, ['leaf: 2', 'root: {a: [2, 3], b: [q, r]}']); + data.update('c', 'test'); + expect(log, ['leaf: 2', 'root: {a: [2, 3], b: [q, r]}', 'root: {a: [2, 3], b: [q, r], c: test}']); + }); } diff --git a/packages/rfw/test_coverage/bin/test_coverage.dart b/packages/rfw/test_coverage/bin/test_coverage.dart index e925d98cd0e3..f5f0dcdc9cd4 100644 --- a/packages/rfw/test_coverage/bin/test_coverage.dart +++ b/packages/rfw/test_coverage/bin/test_coverage.dart @@ -20,9 +20,10 @@ import 'package:meta/meta.dart'; // Please update these targets when you update this package. // Please ensure that test coverage continues to be 100%. -const int targetLines = 3223; +// Don't forget to update the lastUpdate date too! +const int targetLines = 3273; const String targetPercent = '100'; -const String lastUpdate = '2023-06-29'; +const String lastUpdate = '2024-01-30'; @immutable /* final */ class LcovLine { @@ -196,14 +197,14 @@ Future main(List arguments) async { print( 'Total lines of covered code has increased, and coverage script is now out of date.\n' 'Coverage is now $coveredPercent%, $coveredLines/$totalLines lines, whereas previously there were only $targetLines lines.\n' - 'Update the "\$targetLines" constant at the top of rfw/test_coverage/bin/test_coverage.dart (to $coveredLines).', + 'Update the "targetLines" constant at the top of rfw/test_coverage/bin/test_coverage.dart (to $coveredLines).', ); } if (targetLines > totalLines) { print( 'Total lines of code has reduced, and coverage script is now out of date.\n' 'Coverage is now $coveredPercent%, $coveredLines/$totalLines lines, but previously there were $targetLines lines.\n' - 'Update the "\$targetLines" constant at the top of rfw/test_coverage/bin/test_coverage.dart (to $totalLines).', + 'Update the "targetLines" constant at the top of rfw/test_coverage/bin/test_coverage.dart (to $totalLines).', ); exit(1); }