@@ -4,6 +4,7 @@ import 'package:codelessly_api/codelessly_api.dart';
44import 'package:collection/collection.dart' ;
55import 'package:equatable/equatable.dart' ;
66import 'package:flutter/material.dart' ;
7+ import 'package:flutter/scheduler.dart' ;
78
89import '../../codelessly_sdk.dart' ;
910
@@ -85,10 +86,112 @@ class CodelesslyContext with ChangeNotifier, Equatable {
8586
8687 @override
8788 void dispose () {
89+ unbindComputedVariables ();
8890 nodeRebuildNotifier.dispose ();
8991 super .dispose ();
9092 }
9193
94+ /// Listeners that keep computed variables up to date, by the notifier
95+ /// they listen to. See [bindComputedVariables] .
96+ final List <(Listenable , VoidCallback )> _computedListeners = [];
97+
98+ /// The computed variables being evaluated, by id, to stop a cycle from
99+ /// evaluating forever.
100+ final Set <String > _computing = {};
101+
102+ /// Computes the value of every computed variable (a variable with a
103+ /// [VariableData.formula] ) in [variables] and keeps it up to date: each is
104+ /// computed again when a variable its formula reads changes, or when
105+ /// [data] changes. Their values are cached in their notifiers, so bindings
106+ /// read and listen to them like any other variable.
107+ ///
108+ /// Computed variables in a cycle hold an empty value. Call this again when
109+ /// [variables] changes; the previous listeners are removed.
110+ /// See [ComputedVariables] .
111+ void bindComputedVariables () {
112+ unbindComputedVariables ();
113+ final Map <String , VariableData > byName = variableNamesMap ();
114+ if (! byName.values.any ((variable) => variable.isComputed)) return ;
115+ final Set <String > cyclic = ComputedVariables .cyclicNames (byName);
116+ // The variables as declared: a result can change a computed variable's
117+ // type, but each evaluation starts from the declared one.
118+ final Map <String , VariableData > declared = {
119+ for (final VariableData variable in byName.values)
120+ if (variable.isComputed) variable.name: variable,
121+ };
122+
123+ final List <VoidCallback > recomputeAll = [];
124+ final List <Observable <VariableData >> ordered = [
125+ for (final String name in ComputedVariables .evaluationOrder (byName))
126+ ? findVariableByName (name),
127+ ];
128+ for (final Observable <VariableData > notifier in ordered) {
129+ final VariableData variable = declared[notifier.value.name]! ;
130+ if (cyclic.contains (variable.name)) {
131+ notifier.value = ComputedVariables .withResult (variable, null );
132+ continue ;
133+ }
134+ void recompute () => _recompute (notifier, variable);
135+ recompute ();
136+ recomputeAll.add (recompute);
137+ // Listeners are added in evaluation order, so of two computed
138+ // variables reading the same input, the one the other reads updates
139+ // first.
140+ for (final String input in ComputedVariables .inputsOf (variable)) {
141+ final Observable <VariableData >? source = findVariableByName (input);
142+ if (source == null || identical (source, notifier)) continue ;
143+ source.addListener (recompute);
144+ _computedListeners.add ((source, recompute));
145+ }
146+ }
147+ // Formulas can read `data`, which is set while widgets build: compute
148+ // again after the frame, as setting a variable rebuilds its readers.
149+ void onDataChanged () {
150+ void run () {
151+ for (final VoidCallback recompute in recomputeAll) {
152+ recompute ();
153+ }
154+ }
155+
156+ if (SchedulerBinding .instance.schedulerPhase ==
157+ SchedulerPhase .persistentCallbacks) {
158+ SchedulerBinding .instance.addPostFrameCallback ((_) => run ());
159+ } else {
160+ run ();
161+ }
162+ }
163+
164+ addListener (onDataChanged);
165+ _computedListeners.add ((this , onDataChanged));
166+ }
167+
168+ /// Removes the listeners [bindComputedVariables] added.
169+ void unbindComputedVariables () {
170+ for (final (Listenable listenable, VoidCallback listener)
171+ in _computedListeners) {
172+ listenable.removeListener (listener);
173+ }
174+ _computedListeners.clear ();
175+ }
176+
177+ void _recompute (Observable <VariableData > notifier, VariableData declared) {
178+ if (! _computing.add (declared.id)) return ;
179+ try {
180+ final Object ? result = ComputedVariables .evaluate (
181+ declared,
182+ scopedValues: ScopedValues .preview (
183+ variables: const {},
184+ codelesslyContext: this ,
185+ ),
186+ );
187+ notifier.value = ComputedVariables .withResult (declared, result);
188+ } catch (error) {
189+ log ('[CodelesslyContext] Computed variable ${declared .name }: $error ' );
190+ } finally {
191+ _computing.remove (declared.id);
192+ }
193+ }
194+
92195 /// Returns a map of all of the [VariableData] s in [variables] mapped by their
93196 /// name.
94197 Map <String , VariableData > variableNamesMap () =>
@@ -218,12 +321,18 @@ class CodelesslyContext with ChangeNotifier, Equatable {
218321 /// Allows to easily [value] of a variable with a given [name] .
219322 /// Returns false if the variable does not exist.
220323 /// Returns true if the variable was updated successfully.
324+ /// A computed variable (see [VariableData.formula] ) can't be set: this
325+ /// returns false and leaves it unchanged.
221326 bool updateVariable (String name, Object ? value) {
222327 final ValueNotifier <VariableData >? variable = findVariableByName (name);
223328 if (variable == null ) {
224329 log ('[CodelesslyContext] Variable with name $name does not exist.' );
225330 return false ;
226331 }
332+ if (variable.value.isComputed) {
333+ log ('[CodelesslyContext] Variable $name is computed and read-only.' );
334+ return false ;
335+ }
227336 final String newValue = value == null ? '' : '$value ' ;
228337
229338 // If the value is the same, then the underlying value notifier will not
0 commit comments