Pre-built Collection Signals
Jolt provides pre-built reactive collection types, including ListSignal, SetSignal, MapSignal, and IterableSignal. These pre-built collection signals are implemented based on corresponding Mixins, and all modification operations automatically trigger reactive updates. Suitable for scenarios requiring reactive collection operations, such as dynamic lists, tag sets, configuration maps, etc.
import 'package:jolt/jolt.dart';
void main() {
final items = ListSignal(['a', 'b', 'c']);
Effect(() {
print('List: ${items.value}');
});
items.add('d'); // Output: "List: [a, b, c, d]"
}Pre-built Collection Signals
ListSignal
ListSignal provides reactive list operations:
final items = ListSignal(['a', 'b', 'c']);Using extension methods:
final normalList = [1, 2, 3];
final reactiveList = normalList.toListSignal();All list operations automatically trigger updates:
final items = ListSignal([1, 2, 3]);
Effect(() {
print('List length: ${items.length}');
});
items.add(4); // Triggers update
items.insert(0, 0); // Triggers update
items.removeAt(2); // Triggers update
items.clear(); // Triggers updateDirectly replace the entire list:
final items = ListSignal([1, 2, 3]);
items.value = [4, 5, 6]; // Replace entire list, triggers updateSetSignal
SetSignal provides reactive set operations:
final tags = SetSignal({'dart', 'flutter'});Using extension methods:
final normalSet = {'dart', 'flutter'};
final reactiveSet = normalSet.toSetSignal();All set operations automatically trigger updates:
final tags = SetSignal({'a', 'b'});
Effect(() {
print('Tag count: ${tags.length}');
});
tags.add('c'); // Triggers update
tags.remove('a'); // Triggers update
tags.addAll({'d', 'e'}); // Triggers update
tags.clear(); // Triggers updateMapSignal
MapSignal provides reactive map operations:
final user = MapSignal({'name': 'Alice', 'age': 30});Using extension methods:
final normalMap = {'key': 'value'};
final reactiveMap = normalMap.toMapSignal();All map operations automatically trigger updates:
final user = MapSignal({'name': 'Alice'});
Effect(() {
print('User info: ${user.value}');
});
user['age'] = 30; // Triggers update
user.addAll({'city': 'NYC', 'country': 'USA'}); // Triggers update
user.remove('city'); // Triggers update
user.clear(); // Triggers updateIterableSignal
IterableSignal provides reactive iterable operations, implemented based on Computed:
final numbers = Signal([1, 2, 3, 4, 5]);
final evenNumbers = IterableSignal(() =>
numbers.value.where((n) => n.isEven)
);You can use all iterable operations:
final numbers = Signal([1, 2, 3, 4, 5]);
final doubled = IterableSignal(() =>
numbers.value.map((n) => n * 2)
);
Effect(() {
print('Doubled: ${doubled.toList()}');
});Collection Signal Mixins
Jolt provides corresponding Mixins for each collection type. These Mixins implement all collection operations and automatically trigger reactive updates when modified:
ListSignalMixin<E>: Provides reactive list functionalitySetSignalMixin<E>: Provides reactive set functionalityMapSignalMixin<K, V>: Provides reactive map functionalityIterableSignalMixin<E>: Provides reactive iterable functionality
These Mixins implement all interface methods of the corresponding collection types and call notify() after modification operations to notify subscribers.
How Mixins Work
Taking ListSignalMixin as an example:
mixin ListSignalMixin<E>
implements ListBase<E>, Readonly<List<E>>, IMutableCollection {
// Implement all List interface methods
@override
int get length => value.length;
@override
void add(E element) {
peek.add(element);
notify(); // Notify subscribers after modification
}
@override
void removeAt(int index) {
peek.removeAt(index);
notify(); // Notify subscribers after modification
}
// ... other methods
}Mixins access the underlying collection value through value, perform modifications through peek, and call notify() after modifications to trigger reactive updates.
Creating Custom Collection Signals
You can create custom collection signals based on these Mixins, adding additional functionality or constraints.
Example: Validated ListSignal
import 'package:jolt/jolt.dart';
import 'package:jolt/src/jolt/collection/list_signal.dart';
import 'package:jolt/src/jolt/signal.dart';
/// Validated list signal: only allows adding elements that satisfy conditions
class ValidatedListSignal<E> extends SignalImpl<List<E>>
with ListSignalMixin<E>
implements ListSignal<E> {
final bool Function(E element) validator;
ValidatedListSignal(
List<E>? value, {
required this.validator,
super.onDebug,
}) : super(value ?? []);
@override
void add(E element) {
if (!validator(element)) {
throw ArgumentError('Element does not pass validation: $element');
}
super.add(element);
}
@override
void insert(int index, E element) {
if (!validator(element)) {
throw ArgumentError('Element does not pass validation: $element');
}
super.insert(index, element);
}
}
// Usage
final positiveNumbers = ValidatedListSignal<int>(
[],
validator: (n) => n > 0,
);
positiveNumbers.add(5); // OK
positiveNumbers.add(-1); // Throws exceptionExample: SetSignal with Maximum Length
import 'package:jolt/jolt.dart';
import 'package:jolt/src/jolt/collection/set_signal.dart';
import 'package:jolt/src/jolt/signal.dart';
/// Set signal with maximum length limit
class BoundedSetSignal<E> extends SignalImpl<Set<E>>
with SetSignalMixin<E>
implements SetSignal<E> {
final int maxLength;
BoundedSetSignal(
Set<E>? value, {
required this.maxLength,
super.onDebug,
}) : super(value ?? {});
@override
bool add(E element) {
if (value.length >= maxLength && !value.contains(element)) {
throw StateError('Set has reached maximum length: $maxLength');
}
return super.add(element);
}
@override
void addAll(Iterable<E> other) {
final toAdd = other.where((e) => !value.contains(e));
if (value.length + toAdd.length > maxLength) {
throw StateError('Adding these elements would exceed maximum length');
}
super.addAll(other);
}
}
// Usage
final tags = BoundedSetSignal<String>(
{},
maxLength: 5,
);
tags.add('dart'); // OK
tags.addAll({'flutter', 'web', 'mobile', 'desktop'}); // OK (5 total)
tags.add('server'); // Throws exceptionExample: Read-only View MapSignal
import 'package:jolt/jolt.dart';
import 'package:jolt/src/jolt/collection/map_signal.dart';
import 'package:jolt/src/jolt/signal.dart';
/// Read-only view map signal: cannot be modified, only read
class ReadonlyMapSignal<K, V> extends SignalImpl<Map<K, V>>
with MapSignalMixin<K, V>
implements MapSignal<K, V> {
ReadonlyMapSignal(Map<K, V>? value, {super.onDebug}) : super(value ?? {});
@override
void operator []=(K key, V value) {
throw UnsupportedError('Cannot modify readonly map');
}
@override
V? remove(Object? key) {
throw UnsupportedError('Cannot modify readonly map');
}
@override
void clear() {
throw UnsupportedError('Cannot modify readonly map');
}
@override
void addAll(Map<K, V> other) {
throw UnsupportedError('Cannot modify readonly map');
}
}
// Usage
final config = ReadonlyMapSignal<String, dynamic>({
'appName': 'MyApp',
'version': '1.0.0',
});
print(config['appName']); // OK
config['newKey'] = 'value'; // Throws exceptionExample: Custom IterableSignal
import 'package:jolt/jolt.dart';
import 'package:jolt/src/jolt/collection/iterable_signal.dart';
import 'package:jolt/src/jolt/computed.dart';
/// Cached iterable signal: caches transformation results for better performance
class CachedIterableSignal<E, R> extends ComputedImpl<Iterable<R>>
with IterableMixin<R>, IterableSignalMixin<R>
implements IterableSignal<R> {
final Iterable<E> Function() sourceGetter;
final R Function(E element) transform;
List<R>? _cachedResult;
Iterable<E>? _lastSource;
CachedIterableSignal(
this.sourceGetter,
this.transform, {
super.onDebug,
}) : super(() => sourceGetter().map(transform));
@override
Iterable<R> get value {
final source = sourceGetter();
if (_cachedResult == null || _lastSource != source) {
_cachedResult = source.map(transform).toList();
_lastSource = source;
}
return _cachedResult!;
}
}
// Usage
final numbers = Signal([1, 2, 3, 4, 5]);
final squared = CachedIterableSignal<int, int>(
() => numbers.value,
(n) => n * n,
);
print(squared.toList()); // [1, 4, 9, 16, 25]Best Practices
Prefer Pre-built Signals: For most scenarios, the pre-built
ListSignal,SetSignal,MapSignal, andIterableSignalare sufficient.Use Mixins to Extend Functionality: If you need to add validation, constraints, or special behavior, extend
SignalImpl<CollectionType>and use the corresponding Mixin.Maintain Reactive Semantics: Custom collection signals should maintain reactive semantics, calling
notify()after modification operations (Mixins handle this automatically).Implement Required Interfaces: Ensure custom signals implement the corresponding collection interfaces (such as
ListBase,SetBase,MapBase) for compatibility with Dart's standard library.Handle Edge Cases: When implementing custom validation or constraint logic, consider edge cases such as empty collections, maximum length, etc.
Related APIs
- Extending Jolt - Learn how to extend Jolt's core functionality
- Signal - Learn about basic signal usage
- Computed - Learn about computed property usage