Skip to content

Instantly share code, notes, and snippets.

@escamoteur
Last active January 12, 2021 11:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save escamoteur/97283e8602fdc4ccc6e2b670aba06e83 to your computer and use it in GitHub Desktop.
Save escamoteur/97283e8602fdc4ccc6e2b670aba06e83 to your computer and use it in GitHub Desktop.
language proposal

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 whereof 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

  1. add a callId keyword that returns a unique value for every call position in the code.
  2. add a new type of variable static local that keep their value from one call at a calling position to the next
  3. 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.

@chimon2000
Copy link

Looks like some of the code formatting is off

@escamoteur
Copy link
Author

Thanks, fixed

@escamoteur
Copy link
Author

@esDotDev maybe this could be interesting for you too.

@esDotDev
Copy link

Could you provide an example of option 2 or 3 with a future builder?

@escamoteur
Copy link
Author

Could you provide an example of option 2 or 3 with a future builder?

The idea would be that the function providing the future could store the Future it returns the first time and returns that until the future has complreted.

@esDotDev
Copy link

esDotDev commented Dec 28, 2020

I think I understand, but it's tricky with futures cause you need a 2nd var to check if they actually finished. So something like:

  Future _loadData() {
    persistent bool isRunning = false; //This state is somehow magically restored
    persistent Future myFuture;  //Also magically restored
    if(isRunning == false){
       myFuture = model.loadData()..whenComplete(()=>isRunning =false);
       isRunning = true;
    }
    return myFuture;
  }

?

_loadData() == _loadData() // generally prints true, async futures notwithstanding.

@esDotDev
Copy link

esDotDev commented Dec 28, 2020

I can't help by point out that my incoming StatefulProps would handle this fairly easily I think, cause it already keeps a handle to the existing future, it could easily track isComplete, and also provide a shortcut method:
future1.setCached(_loadData()); // Could run this every build, and have it be smart enough to ignore newer ones while loading

@escamoteur
Copy link
Author

But it won't solve the other use cases and would reqire the user of a package to use StatefulProps where my approach would enable the package to make sure it doesn't happen.
I just checked, there seems to be no way to check if a future has already completed

@esDotDev
Copy link

esDotDev commented Dec 28, 2020

I guess this is not so different than a coroutine in Unity, a function that you can return to each frame (or at any other interval), and it has the state, and we have a yield keyword. Something like:

IEnumerator _myCoroutine(){
    int index = 0;
    while(true){
        index++; // This increments 
       yield return WaitOneFrame();
    }
}

In unity we return a time amount like WaitForSeconds(.1) and Unity ticks it for us at the correct time.

But your ask is similar to a coroutine that we tick ourselves whenever we need:

coroutine Future _loadData(){
    bool isLoading= false;
    Future myFuture;
    while(true){
        if(!isLoading){
           isLoading = true;
           myFuture = Future(...).whenComplete(() => isLoading = false);
        }
        //`yield` is special keyword here, it means I'm done until next call. 
        //When called again, we'll just keep going, which means will start at the top of the loop again
        yield return myFuture;
    }
}

@esDotDev
Copy link

esDotDev commented Dec 28, 2020

You initial example might be like:

coroutine ValueListenable<String> getListenable(){
   final n = _notifier.where((x) => x.isNotEmpty);
   while(true){
      yield return n;
   }
}

The trick is how they would be tracked... in Unity we call StartCoroutine(getListenable()) and the engine takes it from there. We still need a handle somehow. Like:

CoroutineTicker_listenableWatcher = CoroutineTicker(watchListenable());
Coroutine watchListenable(){
   ...
}

build(){
    ValueListenable<String>value = _listenableWatcher.tick();
}

@mraleph
Copy link

mraleph commented Jan 12, 2021

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:

void foo() {
  magicBar();  // magicBar uses feature to identify its call-site
}

then you refactored and duplicated call-site to magicBar.

void foo() {
  foo1();
  foo2();
}

void foo1() {
  magicBar();
}

void foo2() {
  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 of C 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.

Stream<int> foo() async* {
  yield 1; 
  yield 2; 
  yield 3;
}

Iterable<int> bar() sync* {
  yield 1; 
  yield 2; 
  yield 3;
}

@escamoteur
Copy link
Author

@mraleph

things (might) start behaving different.
That I exactly what I expect. it should allow to behave differently when called at different places. I'm completely aware that this is a dangerous feature but it would allow library authors to actually make functions more safe when used in a reactive environment.
And as listed above, neither in extension methods, nor in case of Hooks there is a clean way to store the values explicitly.

Do you think it makes sense to post it in the language repo?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment