Computed
Computed is a lazy derived value in the reactive system. It only recomputes when subscribed to and when dependencies change, with automatic caching that efficiently handles expensive computations.
import 'package:jolt/jolt.dart';
void main() {
// Create dependent signals
final firstName = Signal('John');
final lastName = Signal('Doe');
// Create computed value
final fullName = Computed(() => '${firstName.value} ${lastName.value}');
// Subscribe to computed value changes
Effect(() {
print('Full name: ${fullName.value}');
});
// Modify dependencies
firstName.value = 'Jane'; // Output: "Full name: Jane Doe"
}Creation
Create a computed value using the Computed constructor, passing a getter function:
final count = Signal(0);
final doubled = Computed(() => count.value * 2);Computed is lazy—it only computes when accessed. If there are no subscribers, the getter function may never execute.
Custom Equality Comparison
You can provide a custom equality function to control when the computed value is considered "changed". This is useful for complex types like lists or maps where you want to compare by value rather than reference:
final signal = Signal<List<int>>([1, 2, 3]);
final computed = Computed<List<int>>(
() => List<int>.from(signal.value),
equals: (a, b) {
if (a is! List<int> || b is! List<int>) return a == b;
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
},
);
Effect(() {
print('Computed: ${computed.value}');
});
signal.value = [1, 2, 3]; // Same values, different instance
// Effect won't trigger because equals returns trueWhen equals returns true, the computed value is considered unchanged and subscribers won't be notified, even if a new computation occurs.
Reading Values
.value
Use the .value property to read values—this creates reactive dependencies and triggers computation. If dependencies have changed, it recomputes; otherwise, it returns the cached value.
final count = Signal(0);
final doubled = Computed(() => count.value * 2);
Effect(() {
print(doubled.value); // Using .value
});
count.value = 5; // Triggers recomputationYou can also use the call() extension method for a function-like syntax:
final count = Signal(0);
final doubled = Computed(() => count.value * 2);
Effect(() {
print(doubled()); // Using call() extension, equivalent to .value
});
count.value = 5; // Triggers recomputation.peek
Use the .peek property to read computed values—does not establish reactive dependencies but will recompute (if dependencies have changed). This ensures you get the latest computed result without creating dependencies.
final count = Signal(0);
final doubled = Computed(() => count.value * 2);
Effect(() {
final tracked = doubled.value; // Establishes dependency
final untracked = doubled.peek; // Does not establish dependency, but recomputes
});
count.value = 10; // tracked updates, untracked does not trigger Effect.peekCached
Use the .peekCached property to read cached computed values—does not establish reactive dependencies and does not recompute. If dependencies have changed but haven't been accessed, the returned value may be stale.
final count = Signal(0);
final doubled = Computed(() => count.value * 2);
print(doubled.peekCached); // Returns cached value, computes once if cache doesn't exist
count.value = 10; // Dependency changed
print(doubled.peekCached); // Still returns old value (0), because not recomputed
print(doubled.value); // Triggers recomputation, returns new value (20)Difference between peek and peekCached:
peek: Always recomputes (if needed), ensures latest result, but does not establish dependenciespeekCached: Only returns cached value, computes only if cache doesn't exist, more efficient but may return stale values
final expensive = Computed(() => heavyCalculation());
// Need latest value but don't establish dependency
final latest = expensive.peek; // Will recompute
// Only need quick check of cached value
final cached = expensive.peekCached; // Returns cache immediately, no recomputationComputed.getPeek<T>()
The static method Computed.getPeek<T>() allows you to access the pending value (the value being computed) from within a computed getter function. This is useful for advanced scenarios where you need to compare with the previous value or perform mutations.
Important: This method can only be called from within a computed getter function. Calling it outside will throw a StateError.
final signal = Signal(0);
int? previousValue;
final computed = Computed<int>(() {
signal.value; // Track dependency
previousValue = Computed.getPeek<int>(); // Get previous pending value
return signal.value * 2;
});
computed.value; // previousValue is null (first computation)
signal.value = 5;
computed.value; // previousValue is 0 (previous pending value)This is particularly useful when implementing custom logic that needs to compare with the previous computed value or when working with mutable collections.
Manual Notification
If you need to manually tell subscribers that it has updated, you can use the notify() method. The notify() method accepts an optional force parameter:
notify()ornotify(true)(force update): Always notifies subscribers, even if the value hasn't changed. This is the default behavior.notify(false)(soft update): Only notifies subscribers if the computed value actually changed during recomputation.
final count = Signal(0);
final doubled = Computed(() => count.value * 2);
Effect(() {
print('Doubled: ${doubled.value}');
});
count.value = 5; // First output: "Doubled: 10"
doubled.notify(false); // No output (value unchanged)
doubled.notify(true); // Output: "Doubled: 10" (force update)When to use force updates:
- When you need to force subscribers to re-evaluate even if the value appears unchanged
- When working with mutable objects where internal state may have changed
- When using custom equality functions and you want to bypass the equality check
Lifecycle Management
Computed implements the ReadonlyNode interface and has lifecycle management capabilities:
dispose(): Release resources (similar to Signal)isDisposed: Check if disposed (similar to Signal)
Computed.dispose() plays the same role as Signal.dispose(): it eagerly detaches the computed node from its cached dependency graph, marks it as unusable, and prevents stale graph links from staying alive longer than necessary.
Computed values that are no longer used should call dispose() to release resources. If a computed value simply becomes unreachable, GC can eventually reclaim it, but dispose() gives you deterministic teardown of the reactive graph.
This is different from Effect and Watcher: disposing a computed value does not run side-effect cleanup callbacks, because Computed is still a reactive value node rather than an active side-effect node.
Writable Computed Values
WritableComputed allows you to create a computed value that can be both read and written. When written to, it calls the setter function to update underlying dependencies. The setter function executes in a batch, meaning all signal updates in the setter are batched together, and subscribers only receive one notification after all updates complete.
Creating Writable Computed Values
final firstName = Signal('John');
final lastName = Signal('Doe');
final fullName = WritableComputed(
() => '${firstName.value} ${lastName.value}',
(value) {
final parts = value.split(' ');
firstName.value = parts[0];
lastName.value = parts[1];
},
);Reading and Writing
final count = Signal(0);
final doubled = WritableComputed(
() => count.value * 2,
(value) => count.value = value ~/ 2,
);
Effect(() {
print('Count: ${count.value}, Doubled: ${doubled.value}');
});
doubled.value = 10; // Output: "Count: 5, Doubled: 10"Batch Execution
WritableComputed's setter executes in a batch, meaning all dependency updates are batched together:
final a = Signal(0);
final b = Signal(0);
final sum = WritableComputed(
() => a.value + b.value,
(value) {
a.value = value ~/ 2;
b.value = value ~/ 2;
},
);
var effectCount = 0;
Effect(() {
sum.value;
effectCount++;
});
sum.value = 10; // effectCount only increases by 1, not 2
// Because a and b updates are in the same batchType System
WritableComputed<T> implements both Computed<T> and Signal<T> interfaces, so you can use all methods from Signal and Computed.
Use Cases
Derived State
Computed is most commonly used for derived state:
class TodoApp {
final todos = Signal<List<Todo>>([]);
final filter = Signal<TodoFilter>(TodoFilter.all);
// Derived: filtered todos
final filteredTodos = Computed(() {
final all = todos.value;
switch (filter.value) {
case TodoFilter.all:
return all;
case TodoFilter.active:
return all.where((t) => !t.completed).toList();
case TodoFilter.completed:
return all.where((t) => t.completed).toList();
}
});
// Derived: statistics
final stats = Computed(() {
final all = todos.value;
return TodoStats(
total: all.length,
active: all.where((t) => !t.completed).length,
completed: all.where((t) => t.completed).length,
);
});
}Two-Way Binding
WritableComputed is suitable for scenarios requiring two-way binding:
class FormField {
final _value = Signal('');
// Writable computed value: formatted display
final displayValue = WritableComputed(
() => _value.value.toUpperCase(),
(value) => _value.value = value.toLowerCase(),
);
}Expensive Computations
Computed's caching mechanism makes it perfect for expensive computations:
final data = Signal<List<Data>>([]);
// Expensive computation will be cached
final processed = Computed(() {
return data.value.map((item) {
// Complex processing logic
return expensiveProcessing(item);
}).toList();
});
// Multiple accesses won't recompute
print(processed.value); // Computes once
print(processed.value); // Uses cache
data.value = newData; // Dependency changed
print(processed.value); // RecomputesImportant Notes
Lazy Computation: Computed only computes when accessed. If there are no subscribers, the getter function may never execute.
Caching Mechanism: Computed automatically caches computation results and only recomputes when dependencies change.
Dependency Tracking: Using
.valueorcall()in the getter function to access other reactive values establishes dependencies. Using.peekdoes not establish dependencies.peekvspeekCached:- Use
peekwhen you need the latest value but don't want to establish dependencies - Use
peekCachedwhen you only need a quick check of the cached value
- Use
WritableComputed Batch: All updates in the setter execute in the same batch, and subscribers only receive one notification.
Lifecycle: Computed values that are no longer used should call
dispose()to release resources.
Related APIs
- Signal - Learn about basic signal usage
- Effect - Reactive side effects
- Batch - Batch update mechanism
- Extensions - Computed extension methods