Skip to content

[rfw] Enable subscribing to the root of a DynamicContent #5848

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

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/rfw/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
39 changes: 32 additions & 7 deletions packages/rfw/lib/src/flutter/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you pass the map/list wrapped in an UnmodifiableFoo wrapper so that the user can't manipulate it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would if it was a free abstraction, but given that it's standard practice in Flutter to pass around lists and maps that aren't supposed to be modified, I don't see much point.

/// 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,
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -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<Object> key, SubscriptionCallback callback) {
return _root.subscribe(key, 0, callback);
}
Expand Down Expand Up @@ -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));
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/rfw/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions packages/rfw/test/runtime_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1074,4 +1074,18 @@ void main() {
});
expect(tested, isTrue);
});

testWidgets('DynamicContent subscriptions', (WidgetTester tester) async {
final List<String> log = <String>[];
final DynamicContent data = DynamicContent(<String, Object?>{
'a': <Object>[0, 1],
'b': <Object>['q', 'r'],
});
data.subscribe(<Object>[], (Object value) { log.add('root: $value'); });
data.subscribe(<Object>['a', 0], (Object value) { log.add('leaf: $value'); });
data.update('a', <Object>[2, 3]);
expect(log, <String>['leaf: 2', 'root: {a: [2, 3], b: [q, r]}']);
data.update('c', 'test');
expect(log, <String>['leaf: 2', 'root: {a: [2, 3], b: [q, r]}', 'root: {a: [2, 3], b: [q, r], c: test}']);
});
}
9 changes: 5 additions & 4 deletions packages/rfw/test_coverage/bin/test_coverage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -196,14 +197,14 @@ Future<void> main(List<String> 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);
}
Expand Down