Commit 7020e6cb authored by Ian Hickson's avatar Ian Hickson

Fix Inherited bugs (#3657)

Fixes https://github.com/flutter/flutter/issues/3493

 - rebuild stateless widgets that have dependencies when their ancestors change but they don't

Fixes https://github.com/flutter/flutter/issues/3120

 - rebuild widgets that tried to inherit from a widget that didn't exist, when the widget is added

This adds a pointer and a bool to Element, which isn't great. It also adds a more or less complete tree walk when you add a new Inherited widget at the top of your tree, which isn't cheap.
parent 60725528
...@@ -196,15 +196,10 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -196,15 +196,10 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
super.deactivate(); super.deactivate();
} }
@override
void dependenciesChanged(Type affectedWidgetType) {
if (affectedWidgetType == Theme && _lastHighlight != null)
_lastHighlight.color = Theme.of(context).highlightColor;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(config.debugCheckContext(context)); assert(config.debugCheckContext(context));
_lastHighlight?.color = Theme.of(context).highlightColor;
final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null; final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
return new GestureDetector( return new GestureDetector(
onTapDown: enabled ? _handleTapDown : null, onTapDown: enabled ? _handleTapDown : null,
......
...@@ -451,7 +451,7 @@ abstract class State<T extends StatefulWidget> { ...@@ -451,7 +451,7 @@ abstract class State<T extends StatefulWidget> {
/// Called when an Inherited widget in the ancestor chain has changed. Usually /// Called when an Inherited widget in the ancestor chain has changed. Usually
/// there is nothing to do here; whenever this is called, build() is also /// there is nothing to do here; whenever this is called, build() is also
/// called. /// called.
void dependenciesChanged(Type affectedWidgetType) { } void dependenciesChanged() { }
@override @override
String toString() { String toString() {
...@@ -1120,37 +1120,50 @@ abstract class Element implements BuildContext { ...@@ -1120,37 +1120,50 @@ abstract class Element implements BuildContext {
element.visitChildren(_activateRecursively); element.visitChildren(_activateRecursively);
} }
/// Called when a previously de-activated widget (see [deactivate]) is reused
/// instead of being unmounted (see [unmount]).
void activate() { void activate() {
assert(_debugLifecycleState == _ElementLifecycle.inactive); assert(_debugLifecycleState == _ElementLifecycle.inactive);
assert(widget != null); assert(widget != null);
assert(depth != null); assert(depth != null);
assert(!_active); assert(!_active);
_active = true; _active = true;
// We unregistered our dependencies in deactivate, but never cleared the list.
// Since we're going to be reused, let's clear our list now.
_dependencies?.clear();
_updateInheritance(); _updateInheritance();
assert(() { _debugLifecycleState = _ElementLifecycle.active; return true; }); assert(() { _debugLifecycleState = _ElementLifecycle.active; return true; });
} }
// TODO(ianh): Define activation/deactivation thoroughly (other methods point
// here for details).
void deactivate() { void deactivate() {
assert(_debugLifecycleState == _ElementLifecycle.active); assert(_debugLifecycleState == _ElementLifecycle.active);
assert(widget != null); assert(widget != null);
assert(depth != null); assert(depth != null);
assert(_active); assert(_active);
if (_dependencies != null) { if (_dependencies != null && _dependencies.length > 0) {
for (InheritedElement dependency in _dependencies) for (InheritedElement dependency in _dependencies)
dependency._dependents.remove(this); dependency._dependents.remove(this);
_dependencies.clear(); // For expediency, we don't actually clear the list here, even though it's
// no longer representative of what we are registered with. If we never
// get re-used, it doesn't matter. If we do, then we'll clear the list in
// activate(). The benefit of this is that it allows BuildableElement's
// activate() implementation to decide whether to rebuild based on whether
// we had dependencies here.
} }
_inheritedWidgets = null; _inheritedWidgets = null;
_active = false; _active = false;
assert(() { _debugLifecycleState = _ElementLifecycle.inactive; return true; }); assert(() { _debugLifecycleState = _ElementLifecycle.inactive; return true; });
} }
/// Called after children have been deactivated. /// Called after children have been deactivated (see [deactivate]).
void debugDeactivated() { void debugDeactivated() {
assert(_debugLifecycleState == _ElementLifecycle.inactive); assert(_debugLifecycleState == _ElementLifecycle.inactive);
} }
/// Called when an Element is removed from the tree permanently. /// Called when an Element is removed from the tree permanently after having
/// been deactivated (see [deactivate]).
void unmount() { void unmount() {
assert(_debugLifecycleState == _ElementLifecycle.inactive); assert(_debugLifecycleState == _ElementLifecycle.inactive);
assert(widget != null); assert(widget != null);
...@@ -1168,6 +1181,7 @@ abstract class Element implements BuildContext { ...@@ -1168,6 +1181,7 @@ abstract class Element implements BuildContext {
Map<Type, InheritedElement> _inheritedWidgets; Map<Type, InheritedElement> _inheritedWidgets;
Set<InheritedElement> _dependencies; Set<InheritedElement> _dependencies;
bool _hadUnsatisfiedDependencies = false;
@override @override
InheritedWidget inheritFromWidgetOfExactType(Type targetType) { InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
...@@ -1179,6 +1193,7 @@ abstract class Element implements BuildContext { ...@@ -1179,6 +1193,7 @@ abstract class Element implements BuildContext {
ancestor._dependents.add(this); ancestor._dependents.add(this);
return ancestor.widget; return ancestor.widget;
} }
_hadUnsatisfiedDependencies = true;
return null; return null;
} }
...@@ -1229,9 +1244,7 @@ abstract class Element implements BuildContext { ...@@ -1229,9 +1244,7 @@ abstract class Element implements BuildContext {
ancestor = ancestor._parent; ancestor = ancestor._parent;
} }
void dependenciesChanged(Type affectedWidgetType) { void dependenciesChanged();
assert(false);
}
String debugGetCreatorChain(int limit) { String debugGetCreatorChain(int limit) {
List<String> chain = <String>[]; List<String> chain = <String>[];
...@@ -1402,6 +1415,19 @@ abstract class BuildableElement extends Element { ...@@ -1402,6 +1415,19 @@ abstract class BuildableElement extends Element {
owner._debugCurrentBuildTarget = this; owner._debugCurrentBuildTarget = this;
return true; return true;
}); });
_hadUnsatisfiedDependencies = false;
// In theory, we would also clear our actual _dependencies here. However, to
// clear it we'd have to notify each of them, unregister from them, and then
// reregister as soon as the build function re-dependended on it. So to
// avoid faffing around we just never unregister our dependencies except
// when we're deactivated. In principle this means we might be getting
// notified about widget types we once inherited from but no longer do, but
// in practice this is so rare that the extra cost when it does happen is
// far outweighed by the avoided work in the common case.
// We _do_ clear the list properly any time our ancestor chain changes in a
// way that might result in us getting a different Element's Widget for a
// particular Type. This avoids the potential of being registered to
// multiple identically-typed Widgets' Elements at the same time.
performRebuild(); performRebuild();
assert(() { assert(() {
assert(owner._debugCurrentBuildTarget == this); assert(owner._debugCurrentBuildTarget == this);
...@@ -1411,22 +1437,28 @@ abstract class BuildableElement extends Element { ...@@ -1411,22 +1437,28 @@ abstract class BuildableElement extends Element {
assert(!_dirty); assert(!_dirty);
} }
/// Called by [rebuild] after [rebuild] has checked that this element indeed /// Called by rebuild() after the appropriate checks have been made.
/// needs to build and is still active.
///
/// This function is called if this element is marked as needing to be built
/// (see [markNeedsBuild]). For example, if [State.setState] is called on its
/// associated [State] object or if this element depends on an
/// [InheritedElement] that has itself changed.
void performRebuild(); void performRebuild();
@override @override
void dependenciesChanged(Type affectedWidgetType) { void dependenciesChanged() {
assert(_active);
markNeedsBuild();
}
@override
void activate() {
final bool shouldRebuild = ((_dependencies != null && _dependencies.length > 0) || _hadUnsatisfiedDependencies);
super.activate(); // clears _dependencies, and sets active to true
if (shouldRebuild) {
assert(_active); // otherwise markNeedsBuild is a no-op
markNeedsBuild(); markNeedsBuild();
} }
}
@override @override
void _reassemble() { void _reassemble() {
assert(_active); // otherwise markNeedsBuild is a no-op
markNeedsBuild(); markNeedsBuild();
super._reassemble(); super._reassemble();
} }
...@@ -1619,6 +1651,10 @@ class StatefulElement extends ComponentElement { ...@@ -1619,6 +1651,10 @@ class StatefulElement extends ComponentElement {
@override @override
void activate() { void activate() {
super.activate(); super.activate();
// Since the State could have observed the deactivate() and thus disposed of
// resources allocated in the build function, we have to rebuild the widget
// so that its State can reallocate its resources.
assert(_active); // otherwise markNeedsBuild is a no-op
markNeedsBuild(); markNeedsBuild();
} }
...@@ -1647,9 +1683,9 @@ class StatefulElement extends ComponentElement { ...@@ -1647,9 +1683,9 @@ class StatefulElement extends ComponentElement {
} }
@override @override
void dependenciesChanged(Type affectedWidgetType) { void dependenciesChanged() {
super.dependenciesChanged(affectedWidgetType); super.dependenciesChanged();
_state.dependenciesChanged(affectedWidgetType); _state.dependenciesChanged();
} }
@override @override
...@@ -1683,12 +1719,12 @@ abstract class _ProxyElement extends ComponentElement { ...@@ -1683,12 +1719,12 @@ abstract class _ProxyElement extends ComponentElement {
assert(widget != newWidget); assert(widget != newWidget);
super.update(newWidget); super.update(newWidget);
assert(widget == newWidget); assert(widget == newWidget);
notifyDescendants(oldWidget); notifyClients(oldWidget);
_dirty = true; _dirty = true;
rebuild(); rebuild();
} }
void notifyDescendants(_ProxyWidget oldWidget); void notifyClients(_ProxyWidget oldWidget);
} }
class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement { class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement {
...@@ -1728,7 +1764,7 @@ class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement { ...@@ -1728,7 +1764,7 @@ class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement {
} }
@override @override
void notifyDescendants(ParentDataWidget<T> oldWidget) { void notifyClients(ParentDataWidget<T> oldWidget) {
void notifyChildren(Element child) { void notifyChildren(Element child) {
if (child is RenderObjectElement) { if (child is RenderObjectElement) {
child.updateParentData(widget); child.updateParentData(widget);
...@@ -1771,7 +1807,7 @@ class InheritedElement extends _ProxyElement { ...@@ -1771,7 +1807,7 @@ class InheritedElement extends _ProxyElement {
} }
@override @override
void notifyDescendants(InheritedWidget oldWidget) { void notifyClients(InheritedWidget oldWidget) {
if (!widget.updateShouldNotify(oldWidget)) if (!widget.updateShouldNotify(oldWidget))
return; return;
dispatchDependenciesChanged(); dispatchDependenciesChanged();
...@@ -1784,16 +1820,17 @@ class InheritedElement extends _ProxyElement { ...@@ -1784,16 +1820,17 @@ class InheritedElement extends _ProxyElement {
/// function at other times if their inherited information changes outside of /// function at other times if their inherited information changes outside of
/// the build phase. /// the build phase.
void dispatchDependenciesChanged() { void dispatchDependenciesChanged() {
final Type ourRuntimeType = widget.runtimeType; for (Element dependent in _dependents) {
for (Element dependant in _dependents) { dependent.dependenciesChanged();
dependant.dependenciesChanged(ourRuntimeType);
assert(() { assert(() {
// check that it really is our descendant // check that it really is our descendant
Element ancestor = dependant._parent; Element ancestor = dependent._parent;
while (ancestor != this && ancestor != null) while (ancestor != this && ancestor != null)
ancestor = ancestor._parent; ancestor = ancestor._parent;
return ancestor == this; return ancestor == this;
}); });
// check that it really deepends on us
assert(dependent._dependencies.contains(this));
} }
} }
} }
......
...@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'test_widgets.dart';
class TestInherited extends InheritedWidget { class TestInherited extends InheritedWidget {
TestInherited({ Key key, Widget child, this.shouldNotify: true }) TestInherited({ Key key, Widget child, this.shouldNotify: true })
: super(key: key, child: child); : super(key: key, child: child);
...@@ -18,6 +20,16 @@ class TestInherited extends InheritedWidget { ...@@ -18,6 +20,16 @@ class TestInherited extends InheritedWidget {
} }
} }
class ValueInherited extends InheritedWidget {
ValueInherited({ Key key, Widget child, this.value })
: super(key: key, child: child);
final int value;
@override
bool updateShouldNotify(ValueInherited oldWidget) => value != oldWidget.value;
}
void main() { void main() {
testWidgets('Inherited notifies dependents', (WidgetTester tester) { testWidgets('Inherited notifies dependents', (WidgetTester tester) {
List<TestInherited> log = <TestInherited>[]; List<TestInherited> log = <TestInherited>[];
...@@ -74,4 +86,356 @@ void main() { ...@@ -74,4 +86,356 @@ void main() {
expect(log, equals([first, second])); expect(log, equals([first, second]));
}); });
testWidgets('Update inherited when removing node', (WidgetTester tester) {
final List<String> log = <String>[];
tester.pumpWidget(
new Container(
child: new ValueInherited(
value: 1,
child: new Container(
child: new FlipWidget(
left: new Container(
child: new ValueInherited(
value: 2,
child: new Container(
child: new ValueInherited(
value: 3,
child: new Container(
child: new Builder(
builder: (BuildContext context) {
ValueInherited v = context.inheritFromWidgetOfExactType(ValueInherited);
log.add('a: ${v.value}');
return new Text('');
}
)
)
)
)
)
),
right: new Container(
child: new ValueInherited(
value: 2,
child: new Container(
child: new Container(
child: new Builder(
builder: (BuildContext context) {
ValueInherited v = context.inheritFromWidgetOfExactType(ValueInherited);
log.add('b: ${v.value}');
return new Text('');
}
)
)
)
)
)
)
)
)
)
);
expect(log, equals(<String>['a: 3']));
log.clear();
tester.pump();
expect(log, equals(<String>[]));
log.clear();
flipStatefulWidget(tester);
tester.pump();
expect(log, equals(<String>['b: 2']));
log.clear();
flipStatefulWidget(tester);
tester.pump();
expect(log, equals(<String>['a: 3']));
log.clear();
});
testWidgets('Update inherited when removing node and child has global key', (WidgetTester tester) {
final List<String> log = <String>[];
Key key = new GlobalKey();
tester.pumpWidget(
new Container(
child: new ValueInherited(
value: 1,
child: new Container(
child: new FlipWidget(
left: new Container(
child: new ValueInherited(
value: 2,
child: new Container(
child: new ValueInherited(
value: 3,
child: new Container(
key: key,
child: new Builder(
builder: (BuildContext context) {
ValueInherited v = context.inheritFromWidgetOfExactType(ValueInherited);
log.add('a: ${v.value}');
return new Text('');
}
)
)
)
)
)
),
right: new Container(
child: new ValueInherited(
value: 2,
child: new Container(
child: new Container(
key: key,
child: new Builder(
builder: (BuildContext context) {
ValueInherited v = context.inheritFromWidgetOfExactType(ValueInherited);
log.add('b: ${v.value}');
return new Text('');
}
)
)
)
)
)
)
)
)
)
);
expect(log, equals(<String>['a: 3']));
log.clear();
tester.pump();
expect(log, equals(<String>[]));
log.clear();
flipStatefulWidget(tester);
tester.pump();
expect(log, equals(<String>['b: 2']));
log.clear();
flipStatefulWidget(tester);
tester.pump();
expect(log, equals(<String>['a: 3']));
log.clear();
});
testWidgets('Update inherited when removing node and child has global key with constant child', (WidgetTester tester) {
final List<int> log = <int>[];
Key key = new GlobalKey();
Widget child = new Builder(
builder: (BuildContext context) {
ValueInherited v = context.inheritFromWidgetOfExactType(ValueInherited);
log.add(v.value);
return new Text('');
}
);
tester.pumpWidget(
new Container(
child: new ValueInherited(
value: 1,
child: new Container(
child: new FlipWidget(
left: new Container(
child: new ValueInherited(
value: 2,
child: new Container(
child: new ValueInherited(
value: 3,
child: new Container(
key: key,
child: child
)
)
)
)
),
right: new Container(
child: new ValueInherited(
value: 2,
child: new Container(
child: new Container(
key: key,
child: child
)
)
)
)
)
)
)
)
);
expect(log, equals(<int>[3]));
log.clear();
tester.pump();
expect(log, equals(<int>[]));
log.clear();
flipStatefulWidget(tester);
tester.pump();
expect(log, equals(<int>[2]));
log.clear();
flipStatefulWidget(tester);
tester.pump();
expect(log, equals(<int>[3]));
log.clear();
});
testWidgets('Update inherited when removing node and child has global key with constant child, minimised', (WidgetTester tester) {
final List<int> log = <int>[];
Widget child = new Builder(
key: new GlobalKey(),
builder: (BuildContext context) {
ValueInherited v = context.inheritFromWidgetOfExactType(ValueInherited);
log.add(v.value);
return new Text('');
}
);
tester.pumpWidget(
new ValueInherited(
value: 2,
child: new FlipWidget(
left: new ValueInherited(
value: 3,
child: child
),
right: child
)
)
);
expect(log, equals(<int>[3]));
log.clear();
tester.pump();
expect(log, equals(<int>[]));
log.clear();
flipStatefulWidget(tester);
tester.pump();
expect(log, equals(<int>[2]));
log.clear();
flipStatefulWidget(tester);
tester.pump();
expect(log, equals(<int>[3]));
log.clear();
});
testWidgets('Inherited widget notifies descendants when descendant previously failed to find a match', (WidgetTester tester) {
int inheritedValue = -1;
final Widget inner = new Container(
key: new GlobalKey(),
child: new Builder(
builder: (BuildContext context) {
ValueInherited widget = context.inheritFromWidgetOfExactType(ValueInherited);
inheritedValue = widget?.value;
return new Container();
}
)
);
tester.pumpWidget(
inner
);
expect(inheritedValue, isNull);
inheritedValue = -2;
tester.pumpWidget(
new ValueInherited(
value: 3,
child: inner
)
);
expect(inheritedValue, equals(3));
});
testWidgets('Inherited widget doesn\'t notify descendants when descendant did not previously fail to find a match and had no dependencies', (WidgetTester tester) {
int buildCount = 0;
final Widget inner = new Container(
key: new GlobalKey(),
child: new Builder(
builder: (BuildContext context) {
buildCount += 1;
return new Container();
}
)
);
tester.pumpWidget(
inner
);
expect(buildCount, equals(1));
tester.pumpWidget(
new ValueInherited(
value: 3,
child: inner
)
);
expect(buildCount, equals(1));
});
testWidgets('Inherited widget does notify descendants when descendant did not previously fail to find a match but did have other dependencies', (WidgetTester tester) {
int buildCount = 0;
final Widget inner = new Container(
key: new GlobalKey(),
child: new TestInherited(
shouldNotify: false,
child: new Builder(
builder: (BuildContext context) {
context.inheritFromWidgetOfExactType(TestInherited);
buildCount += 1;
return new Container();
}
)
)
);
tester.pumpWidget(
inner
);
expect(buildCount, equals(1));
tester.pumpWidget(
new ValueInherited(
value: 3,
child: inner
)
);
expect(buildCount, equals(2));
});
} }
...@@ -6,14 +6,6 @@ export PATH="$PWD/bin:$PWD/bin/cache/dart-sdk/bin:$PATH" ...@@ -6,14 +6,6 @@ export PATH="$PWD/bin:$PWD/bin/cache/dart-sdk/bin:$PATH"
# analyze all the Dart code in the repo # analyze all the Dart code in the repo
flutter analyze --flutter-repo flutter analyze --flutter-repo
# generate and analyze our large sample app
dart dev/tools/mega_gallery.dart
(cd dev/benchmarks/mega_gallery; flutter analyze --watch --benchmark)
# keep the rest of this file in sync with
# //chrome_infra/build/scripts/slave/recipes/flutter/flutter.py
# see https://github.com/flutter/flutter/blob/master/infra/README.md
# run tests # run tests
(cd packages/flutter; flutter test) (cd packages/flutter; flutter test)
(cd packages/flutter_driver; dart -c test/all.dart) (cd packages/flutter_driver; dart -c test/all.dart)
...@@ -27,3 +19,7 @@ dart dev/tools/mega_gallery.dart ...@@ -27,3 +19,7 @@ dart dev/tools/mega_gallery.dart
(cd examples/layers; flutter test) (cd examples/layers; flutter test)
(cd examples/material_gallery; flutter test) (cd examples/material_gallery; flutter test)
(cd examples/stocks; flutter test) (cd examples/stocks; flutter test)
# generate and analyze our large sample app
dart dev/tools/mega_gallery.dart
(cd dev/benchmarks/mega_gallery; flutter analyze --watch --benchmark)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment