Repository navigation
Avoid calling didChangeDependences on a State that has dropped out of the tree - #49527
Conversation
|
It is a breaking change This solves the "Avoid calling didChangeDependences on a State that has dropped out of the tree". If a StatefulWidget depends on two Inheritedwidgets, and both Inheritedwidgets changes within the same frame, then the StatefulWidget's didChangeDependencies is currently called twice |
|
Hm. Should be easy to unbreak that by using a count instead of a book. |
|
But it seems like we should eventually break it. |
|
whether it's a breaking change or not is just whether it breaks any of the contributed tests. (see our breaking change policy) assuming it doesn't, this seems valuable. |
|
I don't know about the contributed tests, but it does break some of the content available online. I've seen @override
void didChangeDependencies() {
super.didChangeDependencies();
if (whatever) {
showBottomSheet<void>(
context: context,
builder: (_) {
return Text('Hello world');
},
);
}
} |
|
I know about flutter/test, but it does not consider all the private apps, articles, and answers online. I know this for fact because a few people asked me about "how to push Routes/... on provider change" and I remember answering a few of these with Here's one for example: https://stackoverflow.com/questions/59400368/how-to-do-something-if-changenotifier-notifies-with-provider-package-similar So even if all tests on Flutter and Flutter/test pass, there's still a risk here. |
|
This looks like a good change to me, but it is failing some tests on Cirrus. |
|
Yeah, it's breaking some stuff with focus scope, trying to figure out why right now. |
|
To make this work, I had to update FocusScope so that it does some work it used to only do in didChangeDependencies so that it now does that work also in deactivate. I think this falls under:
|
| @override | ||
| void deactivate() { | ||
| super.deactivate(); | ||
| _focusAttachment?.reparent(); |
There was a problem hiding this comment.
@gspencergoog -this makes tests pass, and the autofocus stuff will happen in didChangeDependencies if the node gets reactivated.
Is this expensive? Should we be concerned about doing more work here in the case of deactivation?
I've also updated the test in framework_tests to make sure that we do in fact call didChangeDependencies when we deactivate an element and then reinsert it to the tree.
There was a problem hiding this comment.
This is not terribly expensive, and needs to be done, regardless.
There was a problem hiding this comment.
Can you add a comment here as to why we reparent on deactivate?
From talking to @gspencergoog: We can't unfocus or remove the node from the tree because if the widget is moved to a different part of the tree (via global key) it should retain its focus state. That's why we temporarily park it on the root focus node (via reparent) until it either gets moved to a different part of the tree (via didChangeDependencies) or until it is disposed.
| @override | ||
| void deactivate() { | ||
| super.deactivate(); | ||
| _focusAttachment?.reparent(); |
There was a problem hiding this comment.
This is not terribly expensive, and needs to be done, regardless.
@rrousselGit Thank you for considering this case - I'm among the ones who used that advice. Would you mind suggesting an alternative that scenario? |
|
This would only break you if your widget was dropping out of the tree AFAICT. If you didn't want to push a route in |
|
I will be running this patch through a set of internal tests before landing - we have plenty of people using provider and if it breaks anything in there it's a breaking change. |
No, this breaks a different situation unrelated to unmounting It's about the lifecycle being called in a state where the tree is unlocked VS the tree being locked. With such change, calling |
|
All in all, I'm definitely for this change. It's more about making sure that it's communicated, as I'm 99% sure that it may break someone's code |
Feel free to submit tests that verify those (e.g. by creating a repo that just contains tests that reflect what those articles say, and submitting that repo to flutter/tests). That would be really useful in fact. |
|
I ran this through all the internal tests and it passed. I'm a bit torn on whether I should be trying to find a way to avoid having this be withint he build scope. |
It shouldn't be my responsibility but either the authors of these articles/answers or some member of the Flutter team when triaging questions/issues.
Here's a test that should fail with these changes: import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
void main() {
testWidgets('didChangeDependencies can modify parents', (tester) async {
final child = MaterialApp(home: Scaffold(body: Example()));
await tester.pumpWidget(Provider.value(value: 0, child: child));
expect(find.text('bottom-sheet'), findsNothing);
await tester.pumpWidget(Provider.value(value: 42, child: child));
expect(find.text('bottom-sheet'), findsOneWidget);
});
}
class Example extends StatefulWidget {
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
int previous;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final value = Provider.of<int>(context);
if (value != previous) {
previous = value;
if (value == 42) {
showBottomSheet<void>(
context: context,
builder: (context) {
return const Text('bottom-sheet');
},
);
}
}
}
@override
Widget build(BuildContext context) {
return Container();
}
} |
|
This test case seems really fishy to me. Even without these changes, it will fail if you change the order of the condition so that it's true on the first build. With these changes, it will fail on both. Without these changes, I'm confused about the expected behavior of It does not show the modal, and does not error (?!). I've tried a couple ways to disable the debug check in this case, but there's no clean way to do it so showing the modal works, because it wants to set state on other elements in the tree, and we can't figure all of that out without going through a build cycle, and by the time we know it's too late to be modifying other parts of the tree. And given that this has several ways to fail, some of which just fail silently, it seems better to have it just error now rather than have it sometimes succeed and sometimes do nothing. |
|
Well, fishy or not that's still something that some people do.
It's currently the only solution to have an "onChange" callback for
inherited widgets that can trigger side-effects, without relying on
addPostFrameCallback
In any case, I have proper solutions for these in progress (like
https://github.com/rrousselGit/flutter_portal), so I'm definitely for these
changes too
…On Tue, Jan 28, 2020, 09:44 Dan Field ***@***.***> wrote:
This test case seems really fishy to me.
Even without these changes, it will fail if you change the order of the
condition so that it's true on the first build. With these changes, it will
fail on both.
Without these changes, I'm confused about the expected behavior of
await tester.pumpWidget(Provider.value(value: 42, child: const SizedBox()));
It does not show the modal, and does not error (?!).
I've tried a couple ways to disable the debug check in this case, but
there's no clean way to do it so this example works, because it wants to
set state on *other elements in the tree*, and we can't figure all of
that out without going through a build cycle, and by the time we know it's
too late to be modifying other parts of the tree.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#49527?email_source=notifications&email_token=AEZ3I3O5JLNKP3IUZATEUMLQ7746BA5CNFSM4KL4TC2KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKCU3CY#issuecomment-579161483>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AEZ3I3JKOCEERLSUMN74KADQ7746BANCNFSM4KL4TC2A>
.
|
|
Proper solution is using yet another library? :( |
|
didChangeDependencies is just like build, in that it shouldn't have side effects like showing a dialog. I'm surprised that doesn't assert, to be honest. |
|
It asserts if:
It does not assert if:
But this PR will change that. @SpajicM |
|
IMO the proper fix should instead be in the line of what's done with Navigator 2.0, by making modals declarative, like I've done in https://github.com/rrousselGit/flutter_portal That's off-topic though. |
|
@rrousselGit - it would probably be good to file a new issue for that, maybe tag @chunhtai |
| @override | ||
| void deactivate() { | ||
| super.deactivate(); | ||
| _focusAttachment?.reparent(); |
There was a problem hiding this comment.
Can you add a comment here as to why we reparent on deactivate?
From talking to @gspencergoog: We can't unfocus or remove the node from the tree because if the widget is moved to a different part of the tree (via global key) it should retain its focus state. That's why we temporarily park it on the root focus node (via reparent) until it either gets moved to a different part of the tree (via didChangeDependencies) or until it is disposed.
| /// This controls whether we should call [State.didChangeDependencies] from | ||
| /// the start of [build], to avoid calls when the [State] will not get built. | ||
| /// This can happen when the widget has dropped out of the tree, but depends | ||
| /// on an [InheritedWidget] that is still in the tree. |
There was a problem hiding this comment.
How would this possible? if an element dropped out the tree, wouldn't it call deactivate to remove the subscription?
There was a problem hiding this comment.
oh i see, it happens before the subtree is built
|
I've added an answer to the linked stackoverflow question indicating why the behavior isn't safe to use this way and won't work going forward, and what to do about it. I'll also send a curteously email to flutter-announce. |
|
After some thinking, I've realized that this PR broke While the behavior change is good, I think it broke the semantics of the life-cycles. Before, we had a very clear separation:
But now things got entangled in a weird way, where My suggestion is: Step 1: Revert to the old behavior of Step 2: Introduce a new method: Step 3: Migrate the previous This achieves the same behavior as right now, but leads to a more logical API and no longer |
|
Can you share some test cases that this breaks? If we revert we should add tests showing what it breaks. |
|
Sure thing: testWidgets('can decorate StatefulElement.build', (tester) async {
await tester.pumpWidget(
Provider.value(
value: 0,
child: HookWidget(),
),
);
// will crash inside didChangeDependencies
await tester.pumpWidget(
Provider.value(
value: 42,
child: HookWidget(),
),
);
});
class HookWidget extends StatefulWidget {
@override
_HookWidgetState createState() => _HookWidgetState();
@override
_HookElement createElement() {
return _HookElement(this);
}
}
class _HookElement extends StatefulElement {
_HookElement(StatefulWidget widget) : super(widget);
static _HookElement _buildingElement;
@override
Widget build() {
try {
_buildingElement = this;
final res = super.build();
return res;
} finally {
_buildingElement = null;
}
}
}
class _HookWidgetState extends State<HookWidget> {
@override
void didChangeDependencies() {
assert(_HookElement._buildingElement == null);
Provider.of<int>(context);
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
assert(_HookElement._buildingElement != null);
return Container();
}
}This breaks some health checks on This is more about the implementation. It feels off to have: Widget build() {
if (_didChangeDependencies) {
_didChangeDependencies = false;
didChangeDependencies();
}
return _state.build();
}IMO it should be more something like: void performRebuild() {
...
Widget built;
try {
if (_didChangeDependencies) {
_didChangeDependencies = false;
didChangeDependencies();
}
built = build();
debugWidgetBuilderValue(widget, built);
}
...This way we are not mixing build and didChangeDependencies |
|
Am I understanding your test correctly by thinking it's basically the same as what you're requesting in #49505? |
|
@rrousselGit I would like to encourage you to contribute your Looking at the code you posted above I don't fully understand the purpose of that test. You could just move the try/finally from As it stands, I don't see any reason to revert this patch. |
|
Actually flutter_mobx broke too – with a more important bug for this one (with basically the same test).
That's impossible. And these answers miss the point. What do you think of: |
That's not clear from the code you posted. Anyhow, it looks like you found a solution. |
Description
We document
didChangeDependenciesas being called right before build - but it's also getting called even ifbuildnever gets called. This is likely to cause extra work for widgets that subscribe to inherited widgets and then drop out of the tree, particularly if those widgets did not realize the expensive work being done indidChangeDependencieswas being done even though they'd never build.Related Issues
Fixes #34130 (/cc @rrousselGit - the implementation here is a slight modification of what you suggested in #34130 (comment))
Tests
I added the following tests:
Test that asserts
didChangeDependenciesis not called after a widget drops out of the tree.Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]). This will ensure a smooth and quick review process.///).flutter analyze --flutter-repo) does not report any problems on my PR.Breaking Change
Did any tests fail when you ran them? Please read Handling breaking changes.