Skip to content

Add a way to know if a ComponentElement is executing its build method or not #49505

Closed

Description

@rrousselGit

Use case

provider's upcoming feature, select, is a way to consume InheritedWidgets that works only inside the build method.

Trying to call select inside other life-cycles (including didChangeDependencies) is not supported.

To make sure that people do not mistakenly still use it in a place where it shouldn't be used, provider would like to add an "assert" that fails if the BuildContext is not in the progress of building.

Proposal

This would be implemented through a debug-only flag on ComponentElement.
Basically, within build that flag would be true, and outside of build it wouldn't.

Then, select could do:

extension Select on BuildContext {
  R select<T, R>(R cb(T value)) {
    assert(this is ComponentElement && this.debugIsInsideBuild);
    ...
  }
}

Activity

  1. dnfield commented on Jan 26, 2020

    @dnfield
    Contributor

    I'm pretty sure you can already do this right? this.owner.debugBuilding I think is what you want.

  2. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jan 26, 2020
  3. rrousselGit commented on Jan 26, 2020

    @rrousselGit
    ContributorAuthor

    No, debugBuilding is global for the entire tree

    This flag would be specific to a given element and represent StatelessWidget.build/StatefulWidget.build, which means it is false during initState/didChangeDependencies/...

  4. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jan 26, 2020
  5. dnfield commented on Jan 26, 2020

    @dnfield
    Contributor

    But once you're debugBuilding, all calls to initState and didChangeDependencies have to be finished anyway for all elements in the tree.

    You don't know if you're in the build for your specific element, but you know you're after that phase of the lifecycle.

  6. dnfield commented on Jan 26, 2020

    @dnfield
    Contributor

    Per the documentation on buildScope, which sets debugBuilding:

      /// Only one [buildScope] can be active at a time.
      ///
      /// A [buildScope] implies a [lockState] scope as well.
    

    lockState means no one can call setState. All state has to be finalized for this cycle by then.

  7. rrousselGit commented on Jan 26, 2020

    @rrousselGit
    ContributorAuthor

    You don't know if you're in the build for your specific element

    But that is specifically what I want

    But once you're debugBuilding, all calls to initState and didChangeDependencies have to be finished anyway for all elements in the tree.

    Unrelated, but that's not true
    I already use debugBuilding for another assertion, and it is true during initState/...

  8. rrousselGit commented on Jan 26, 2020

    @rrousselGit
    ContributorAuthor

    A table of their difference:

    lifecycle owner.debugBuilding debugIsInsideBuild
    initState true false
    didChangeDependencies true false
    build true true
    event handlers (click) false false
  9. changed the title [-]Add a way to know if a `ComponentElement` is building or not[/-] [+]Add a way to know if a `ComponentElement` is executing its build method or not[/+] on Jan 26, 2020
  10. dnfield commented on Jan 26, 2020

    @dnfield
    Contributor

    Ahh ok. Why do you specifically need to know if you're in build though? Could you explain a bit more about the use case you want to support and what code won't work without this check (or what code is more dangerous without this check)?

  11. rrousselGit commented on Jan 27, 2020

    @rrousselGit
    ContributorAuthor

    It is explained in the topmost comment.
    It's also showcased in the following test of the branch that implement the said feature: https://github.com/rrousselGit/provider/blob/677173b78dbe9befb331d10b7438c8b04108e5a2/test/context_test.dart#L283

    This test showcase how without this flag, the library has no way of preventing users from calling select inside didChangeDependencies, which then fails silently.

  12. dnfield commented on Jan 27, 2020

    @dnfield
    Contributor

    All the top most comment says is that a new feature called select wants this. Why?

    A quick look at the code doesn't really make it obvious to me . What about select makes it unsafe to call from didChangeDependencies?

  13. rrousselGit commented on Jan 27, 2020

    @rrousselGit
    ContributorAuthor

    For more information on the feature in itself, see https://twitter.com/remi_rousselet/status/1218180956139540480?s=19

    It's based on a custom Inheritedwidget which works similarly to an InheritedModel.
    But the "aspect" is a callback.

    Since it's used almost always with a new closure instance, it means that the InheritedElement needs a way to somehow clear the outdated dependencies when the dependent rebuild.

    Otherwise as the dependent keeps rebuilding, it'd end up stacking hundreds of different "Selector", which would all be executed whenever the Inheritedwidget may want to notify dependents.

    I did it by clearing the previous callbacks if select is called in a new frame (tracked using addPostFrameCallback).

    This works fine. But it means that dependencies added inside didChangeDependencies may get cleared with no way of being re-added.

    It is not a problem (as the closure acts as a didChangeDependencies if needed) but may be unexpected to some users.

  14. rrousselGit commented on Jan 27, 2020

    @rrousselGit
    ContributorAuthor

    This could be summarised as "to work, select relies on a workaround to #12992 which makes it incompatible with didChangeDependencies".

    To begin with, the reason #12992 can't be fixed easily is because of didChangeDependencies.
    This feature purposefully gave up on that life-cycle to work.

  15. 4 remaining items

  16. rrousselGit commented on Jan 27, 2020

    @rrousselGit
    ContributorAuthor

    The proposed reason to support this API seems dangerous to me. select looks like a cheap method on BuildContext to only rebuild when certain properties on an object you care about change, but it actually ends up adding more work each and every frame per property you select (?!), even if you use it correctly so that it is only called within build methods.

    It's not every frame.
    It's only on InheritedElement.notifyClient.

  17. dnfield commented on Jan 27, 2020

    @dnfield
    Contributor

    Ok - but that could get called every frame if you decide to update your model every frame for some reason, such as having it tied to some animated value.

    Basically, it makes it very easy to end up doing something that's very expensive - and it seems like it could be rewritten in such a way where it could still deliver a lot of value but not make the very expensive thing so easy to do.

  18. rrousselGit commented on Jan 27, 2020

    @rrousselGit
    ContributorAuthor

    I don't see how that is more expensive than what InheritedModel does in general

    It is almost identical to:

    class MyModel extends InheritedModel<bool Function(MyModel m)> {
      @override
      bool updateShouldNotify(MyModel oldWidget) => true;
    
      @override
      bool updateShouldNotifyDependent(
        InheritedModel<bool Function(MyModel m)> oldWidget,
        Set<bool Function(MyModel m)> dependencies,
      ) {
        return dependencies.any((cb) => cb(this));
      }
    }

    the extra code is because of #12992 and for some health checks

    and it seems like it could be rewritten in such a way where it could still deliver a lot of value but not make the very expensive thing so easy to do.

    How? 😄

  19. rrousselGit commented on Jan 27, 2020

    @rrousselGit
    ContributorAuthor

    Ironically, the changes in #49527 kind of put this debug flag in an awkward position.

    It wouldn't be possible to implement such a flag directly from ComponentElement anymore if State.didChangeDependencies is called from inside ComponentElement.build.

    I suppose I'll just move it to StateElement instead

    I'm still curious on how you'd implement such feature @dnfield though 😃

  20. dnfield commented on Jan 28, 2020

    @dnfield
    Contributor

    This is part of why it's not always ideal to expose flags like this - the change in #49527 is something that should help runtime performance.

    As far as how to implement this - I would avoid encouraging people registering multiple callbacks Each select call right now triggers a whole bunch of notifications - instead it could either track state for the user or require that it only be called once with all of the relevant properties you want to subscribe to.

  21. rrousselGit commented on Jan 31, 2020

    @rrousselGit
    ContributorAuthor

    The documentation is enough to discourage people using many selectors at once.

    But having the ability to have multiple ones is critical and there's no reason to remove it.

    Its better not to have such debug flag than to remove the ability to call select multiple times.

  22. escamoteur commented on Jan 31, 2020

    @escamoteur
    Contributor

    Could other users besides provider benefit from this change?

  23. rrousselGit commented on Jan 31, 2020

    @rrousselGit
    ContributorAuthor

    flutter_hook needed the exact same flag too. Although in that very specific case, I was able to implement it myself.

  24. rrousselGit commented on Jan 31, 2020

    @rrousselGit
    ContributorAuthor

    Although now that I think about it, the changes in #49527 means flutter_hooks will need this flag too, as the one I hacked myself will no longer work.

  25. github-actions commented on Aug 22, 2021

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  26. locked as resolved and limited conversation to collaborators on Aug 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    frameworkflutter/packages/flutter repository. See also f: labels.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions