Skip to content

Commit 5b48c44

Browse files
authored
[rfw] Enable subscribing to the root of a DynamicContent (flutter#5848)
fixes flutter/flutter#141069
1 parent c0cb0ee commit 5b48c44

File tree

5 files changed

+58
-14
lines changed

5 files changed

+58
-14
lines changed

packages/rfw/CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
## 1.0.21
2+
3+
* Adds support for subscribing to the root of a `DynamicContent` object.
4+
15
## 1.0.20
26

3-
* Adds OverflowBox material widget.
4-
* Updates ButtonBar material widget implementation.
7+
* Adds `OverflowBox` material widget.
8+
* Updates `ButtonBar` material widget implementation.
59

610
## 1.0.19
711

packages/rfw/lib/src/flutter/content.dart

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import 'package:flutter/foundation.dart' show objectRuntimeType;
1212
import '../dart/model.dart';
1313

1414
/// Signature for the callback passed to [DynamicContent.subscribe].
15+
///
16+
/// Do not modify the provided value (e.g. if it is a map or list). Doing so
17+
/// would leave the [DynamicContent] in an inconsistent state.
1518
typedef SubscriptionCallback = void Function(Object value);
1619

1720
/// Returns a copy of a data structure if it consists of only [DynamicMap]s,
@@ -116,9 +119,12 @@ Object? deepClone(Object? template) {
116119
/// [missing] as the new value. It is not an error to subscribe to missing data.
117120
/// It _is_ an error to add [missing] values to the data model, however.
118121
///
122+
/// To subscribe to the root of the [DynamicContent], use an empty list as the
123+
/// key when subscribing.
124+
///
119125
/// The [LocalWidgetBuilder]s passed to a [LocalWidgetLibrary] use a
120126
/// [DataSource] as their interface into the [DynamicContent]. To ensure the
121-
/// integrity of the update mechanism, that interface only allows access to
127+
/// integrity of the update mechanism, _that_ interface only allows access to
122128
/// leaves, not intermediate nodes (maps and lists).
123129
///
124130
/// It is an error to subscribe to the same key multiple times with the same
@@ -143,6 +149,13 @@ class DynamicContent {
143149
/// key.
144150
///
145151
/// Existing keys that are not present in the given map are left unmodified.
152+
///
153+
/// If the root node has subscribers (see [subscribe]), they are called once
154+
/// per key in `initialData`, not just a single time.
155+
///
156+
/// Collections (maps and lists) in `initialData` must not be mutated after
157+
/// calling this method; doing so would leave the [DynamicContent] in an
158+
/// inconsistent state.
146159
void updateAll(DynamicMap initialData) {
147160
for (final String key in initialData.keys) {
148161
final Object value = initialData[key] ?? missing;
@@ -156,6 +169,10 @@ class DynamicContent {
156169
///
157170
/// The `value` must consist exclusively of [DynamicMap], [DynamicList], [int],
158171
/// [double], [bool], and [String] objects.
172+
///
173+
/// Collections (maps and lists) in `value` must not be mutated after calling
174+
/// this method; doing so would leave the [DynamicContent] in an inconsistent
175+
/// state.
159176
void update(String rootKey, Object value) {
160177
_root.updateKey(rootKey, deepClone(value)!);
161178
_scheduleCleanup();
@@ -167,7 +184,14 @@ class DynamicContent {
167184
/// The value is always non-null; if the value is missing, the [missing]
168185
/// object is used instead.
169186
///
187+
/// The empty key refers to the root of the [DynamicContent] object (i.e.
188+
/// the map manipulated by [updateAll] and [update]).
189+
///
170190
/// Use [unsubscribe] when the subscription is no longer needed.
191+
///
192+
/// Do not modify the value returned by this method or passed to the given
193+
/// `callback` (e.g. if it is a map or list). Changes made in this manner will
194+
/// leave the [DynamicContent] in an inconsistent state.
171195
Object subscribe(List<Object> key, SubscriptionCallback callback) {
172196
return _root.subscribe(key, 0, callback);
173197
}
@@ -329,12 +353,6 @@ class _DynamicNode {
329353
_sendUpdates(value);
330354
}
331355

332-
void _sendUpdates(Object value) {
333-
for (final SubscriptionCallback callback in _callbacks) {
334-
callback(value);
335-
}
336-
}
337-
338356
void updateKey(String rootKey, Object value) {
339357
assert(_value is DynamicMap);
340358
assert(_hasValidType(value));
@@ -345,6 +363,13 @@ class _DynamicNode {
345363
if (_children.containsKey(rootKey)) {
346364
_children[rootKey]!.update(value);
347365
}
366+
_sendUpdates(_value);
367+
}
368+
369+
void _sendUpdates(Object value) {
370+
for (final SubscriptionCallback callback in _callbacks) {
371+
callback(value);
372+
}
348373
}
349374

350375
@override

packages/rfw/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: rfw
22
description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime."
33
repository: https://github.com/flutter/packages/tree/main/packages/rfw
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+rfw%22
5-
version: 1.0.20
5+
version: 1.0.21
66

77
environment:
88
sdk: ">=3.0.0 <4.0.0"

packages/rfw/test/runtime_test.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,4 +1074,18 @@ void main() {
10741074
});
10751075
expect(tested, isTrue);
10761076
});
1077+
1078+
testWidgets('DynamicContent subscriptions', (WidgetTester tester) async {
1079+
final List<String> log = <String>[];
1080+
final DynamicContent data = DynamicContent(<String, Object?>{
1081+
'a': <Object>[0, 1],
1082+
'b': <Object>['q', 'r'],
1083+
});
1084+
data.subscribe(<Object>[], (Object value) { log.add('root: $value'); });
1085+
data.subscribe(<Object>['a', 0], (Object value) { log.add('leaf: $value'); });
1086+
data.update('a', <Object>[2, 3]);
1087+
expect(log, <String>['leaf: 2', 'root: {a: [2, 3], b: [q, r]}']);
1088+
data.update('c', 'test');
1089+
expect(log, <String>['leaf: 2', 'root: {a: [2, 3], b: [q, r]}', 'root: {a: [2, 3], b: [q, r], c: test}']);
1090+
});
10771091
}

packages/rfw/test_coverage/bin/test_coverage.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import 'package:meta/meta.dart';
2020

2121
// Please update these targets when you update this package.
2222
// Please ensure that test coverage continues to be 100%.
23-
const int targetLines = 3223;
23+
// Don't forget to update the lastUpdate date too!
24+
const int targetLines = 3273;
2425
const String targetPercent = '100';
25-
const String lastUpdate = '2023-06-29';
26+
const String lastUpdate = '2024-01-30';
2627

2728
@immutable
2829
/* final */ class LcovLine {
@@ -196,14 +197,14 @@ Future<void> main(List<String> arguments) async {
196197
print(
197198
'Total lines of covered code has increased, and coverage script is now out of date.\n'
198199
'Coverage is now $coveredPercent%, $coveredLines/$totalLines lines, whereas previously there were only $targetLines lines.\n'
199-
'Update the "\$targetLines" constant at the top of rfw/test_coverage/bin/test_coverage.dart (to $coveredLines).',
200+
'Update the "targetLines" constant at the top of rfw/test_coverage/bin/test_coverage.dart (to $coveredLines).',
200201
);
201202
}
202203
if (targetLines > totalLines) {
203204
print(
204205
'Total lines of code has reduced, and coverage script is now out of date.\n'
205206
'Coverage is now $coveredPercent%, $coveredLines/$totalLines lines, but previously there were $targetLines lines.\n'
206-
'Update the "\$targetLines" constant at the top of rfw/test_coverage/bin/test_coverage.dart (to $totalLines).',
207+
'Update the "targetLines" constant at the top of rfw/test_coverage/bin/test_coverage.dart (to $totalLines).',
207208
);
208209
exit(1);
209210
}

0 commit comments

Comments
 (0)