Cells
A cell, denoted by the base type ValueCell
, is an object with a
value and a set of observers that react to changes in its value,
you'll see exactly what that means in a moment.
final a = 1.cell;
final b = 'hello world'.cell;
final c = ValueCell.value(someValue);
The above is an example of constant cells, which can be created
either using the .cell
property or the ValueCell.value
constructor
which takes a value and wraps it in a ValueCell
. A constant cell has
a value that does not change throughout its lifetime.
The value of a cell is accessed using the value
property.
print(a.value); // Prints: 1
print(b.value); // Prints: 'hello world'
print(c.value); // Prints the value of `someValue`
Mutable Cells
Mutable cells, created with the MutableCell
constructor, hold a
value that can be set directly, by assigning a value to the value
property.
final a = MutableCell(0);
print(a.value); // Prints: 0
a.value = 3;
print(a.value); // Prints: 3
Observing Cells
When the value of a cell changes, its observers are notified of the
change. The simplest way to demonstrate this is to set up a watch
function using ValueCell.watch
:
final a = MutableCell(0);
final b = MutableCell(1);
// Set up a watch function observing cells `a` and `b`
final watcher = ValueCell.watch(() {
print('${a()} + ${b()} = ${a() + b()}');
});
a.value = 5; // Prints: 5 + 1 = 6
b.value = 10; // Prints: 5 + 10 = 15
In the example above, a watch function that prints the values of cells
a
and b
to the console, along with their sum, is defined. This
function is called automatically when the value of either a
or b
changes.
the value of a cell is referenced using the function call syntax, within a watch function, rather than accessing the value property directly.
Every call to ValueCell.watch
adds a new watch function, for
example:
final watcher2 = ValueCell.watch(() => print('A = ${a()}'));
// Prints: 20 + 10 = 30
// Also prints: A = 20
a.value = 20;
// Prints: 20 + 1 = 21
b.value = 1;
The watch function defined above, watcher2
, observes the value of
a
only. Changing the value of a
results in both watch functions
being called. Changing the value of b
only results in the first
watch function being called, since the second watch function is not
observing b
.
When you no longer need the watch function to be called, call stop
on the CellWatcher
object returned by ValueCell.watch
.
The
Watch
constructor allows you to define a watch function which has access to
its own handle. This allows you to define a watch function that can be
stopped from within the function itself:
final a = MutableCell(0);
Watch((handle) {
print('A = ${a()}')
if (a() > 10) {
handle.stop();
}
});
In this example a watch function is defined that prints the value of
cell a
to the console. When the value of a
exceeds 10 the watch
function is stopped, by calling stop()
on the handle provided to the
watch function.
a.value = 1; // Prints: A = 1
a.value = 5; // Prints: A = 5
a.value = 11; // Prints: A = 11
// The watch function is stopped at this point
a.value = 7; // Doesn't print anything
The handle also provides an
afterInit()
method, which exits the watch function when it is called during the
first call to the watch function. This is useful when you don't want
the side effects defined in the watch function to run on the initial
"setup" call.
Watch((handle) {
final value = a();
handle.afterInit();
print('A = ${a()}');
});
In this example the value of a
is only printed, when it changes
after the watch function is defined with Watch
. It is not
printed when the watch function is called for the first time to
determine its dependencies.
The watch function must observe at least one cell, by the function
call syntax, before the afterInit()
call. Otherwise, the watch
function will not observe any cells and will never be called.
Computed Cells
A computed cell, defined using ValueCell.computed
, is a cell
with a value that is defined as a function of the values of one or
more argument cells. Whenever the value of an argument cell changes,
the value of the computed cell is recomputed.
final a = MutableCell(1);
final b = MutableCell(2);
final sum = ValueCell.computed(() => a() + b());
In the above example, sum
is a computed cell with the value defined
as the sum of cells a
and b
. The value of sum
is recomputed
whenever the values of either a
or b
change. This is demonstrated
below:
final watcher = ValueCell.watch(() {
print('The sum is ${sum()}');
});
a.value = 3; // Prints: The sum is 5
b.value = 4; // Prints: The sum is 7
In this example:
- A watch function observing the
sum
cell is defined. - The value of
a
is set to3
, which:- Causes the value of
sum
to be recomputed - Calls the watch function defined in 1.
- Causes the value of
- The value of
b
is set to4
, which likewise also results in the sum being recomputed and the watch function being called.
The ValueCell.computed
constructor takes an optional changesOnly
keyword argument, which allows you to control whether the computed
cell notifies its observers if its value hasn't changed after a
recomputation. By default this is false
, which means the computed
cell notifies its observers whenever it's value is recomputed. If
changeOnly
is true
, the cell only notifies its observers if the
new value of the cell is not equal to its previous value.
This is demonstrated with the following example:
final a = MutableCell(0);
final b = ValueCell.computed(() => a() % 2, changeOnly: true);
ValueCell.watch(() => print('${b()}'));
a.value = 1;
a.value = 3;
a.value = 5;
a.value = 6;
a.value = 8;
This results in the following being printed to the console:
0
1
0
Notice only three lines are printed to the console even though the
value of the computed cell argument a
was changed five times.
If changesOnly: true
is omitted from the definition of b
, the
following is printed to the console:
0
1
1
1
0
0
Notice that a new line is printed to the console whenever the value of
a
, which is an argument of b
, is changed. This is because b
notifies its observers whenever the value of its argument a
has
changed even when b
's new value is equal to its previous value.
Batch Updates
The MutableCell.batch
function allows the values of multiple mutable
cells to be set simultaneously. The effect of this is that while the
values of the cells are changed as soon as their value
properties
are set, the observers of the cells are only notified after all the
cell values have been set.
final a = MutableCell(0);
final b = MutableCell(1);
final watcher = ValueCell.watch(() {
print('a = ${a()}, b = ${b()}');
});
// This only prints: a = 15, b = 3
MutableCell.batch(() {
a.value = 15;
b.value = 3;
});
In the example above, the values of a
and b
are set to 15
and
3
respectively, within a MutableCell.batch
. The watch function,
which observes both a
and b
, is only called once after the value
of both a
and b
is set within MutableCell.batch
.
As a result the following is printed to the console:
a = 0, b = 1
a = 15, b = 3
a = 0, b = 1
is printed when the watch function is first defined.a = 15, b = 3
is printed whenMutableCell.batch
returns.
A watch function is always called once immediately after it is set up. This is necessary to determine, which cells the watch function is observing.