Enable functions to store data between calls / know from where they were called
Hi,
this title might sound quite strange but during the development of functional_listeners and get_it_mixin I found that the reactive nature of flutter creates some new challenges. Specifically I mean that build
functions are getting called on any data change which makes patterns that I used in the past suddenly difficult.
Let's take the following class as an example:
class Model {
ValueNotifier<String> _notifier;
ValueListenable<String> get nonEmpty => _notifier.where((x) => x.isNotEmpty);
ValueListenable nonEmptySafe;
Model() {
nonEmptySafe = _notifier.where((x) => x.isNotEmpty);
}
Future<String> makeRestCall() {
return Future.value('Result from ');
}
}
Defining a getter like nonEmpty
is no problem in classic UI systems that will only call it ones from one place in the code.
where
creates a new special ValueNotifier
that the caller can subscribe to.
If we access this inside a widget like:
ValueListenableBuilder<String>(
valueListenable: widget.model.nonEmpty,
builder: (context, s,_) {
it works fine the first time the build function is called. But if the surrounding context gets rebuild it will lead to a new instance of that ValueNotifier is created while the first one still exists and reacts on data changes of _notifier
.
the only way to prevent this is to assign the result of where
to a normal field or variable outside the build function like it is done with nonEmptySafe
.
Because of the same reason it's not a good idea to call a data tranformer like where
of functional_listeners or rxdart directly in a constructor of a widget like:
ValueListenableBuilder<String>(
valueListenable: widget.model.nonEmpty.map((x)=>x.toUpper),
builder: (context, s,_) {
another place where you see mistakes like that is in combination with FutureBuilder
that gets it's future from a method call into the model layer. If the context rebuilds suddenly the Futurebuilder
wait for a new Future
and not for the one it first got. Like if you call makeRestCall()
in the model above. what you really wanted is that the FutureBuilder will wait for the first call and not that it does a new call because someone higher up the widget tree did a setState.
The problem here is that makeRestCall()
cannot do anything against this because it doesn't know that it is called from the excact same place again.
To solve this we would need a possibility that a function is able to store data at the place where it is called that will persist between consecutive calls or that a function could query the adress from where it is called so that it can differentiate different calls and can store data in a static map for instance.
By this where
could only create a new instance of the ValueNotifier
that it has to return on the very first call and just return the same instance on all successive calls.
I could imagine several approaches
- add a
callId
keyword that returns a unique value for every call position in the code. - add a new type of variable
static local
that keep their value from one call at a calling position to the next - add a special keyword
localStorage<T>
that allows to store one object between calls
(option 1 is my least favorite because it would lead to static or global maps to reall store the data)
Especially extension functions could profit of such a feature as they have no way to add a new field to their target types if they needed to store any data.
Another example that would benefit from this is that current "hacks" like flutter_hooks or my get_it_mixin could be implemented in a clean way. Currently we increment an index everytime one of its functions is called that is reset to 0 at the beginning of the build
function so that we can identify which call position we are at and to store necessary data. this also means that this calls always have to be done in the same sequence and can't have any conditionals in it.
With the proposed feature this wouldn't be a problem anymore because every function call could be completely independ of other.
I'm aware that such a feature should only used were no other option exist, but especially package authors could add safety messures to their functions that make them save to call inside build functions.
The main thing that I am struggling to understand around this proposal is: why explicitly persisting state between invocation is considerably worse than persisting it implicitly through a language feature? Proposal really needs to make it more convincing. Dart in general favours readability sometimes to the point of verbosity. With a feature as described you make things very implicit - you (kinda) create a function which does different things depending on where it is called from.
Imagine you have written:
then you refactored and duplicated call-site to
magicBar
.things (might) start behaving different.
FWIW at some point there were discussions around allowing a construct
static new C(...)
- which would create a single instance ofC
when it evaluated for the first time and then just return that value. I think that would give you a tool to achieve what you want (though in a more explicit way).@esDotDev You don't need to invent some special syntax for coroutines. Dart has generators (sync and async ones) for that matter.