Commit 97eaee34 authored by Adam Barth's avatar Adam Barth

Merge pull request #1447 from abarth/reactivate

GlobalKeys should preserve state across tree mutations
parents dd6790fc f31f067e
...@@ -93,10 +93,11 @@ abstract class GlobalKey extends Key { ...@@ -93,10 +93,11 @@ abstract class GlobalKey extends Key {
} }
} }
BuildContext get currentContext => _registry[this]; Element get _currentElement => _registry[this];
Widget get currentWidget => _registry[this]?.widget; BuildContext get currentContext => _currentElement;
Widget get currentWidget => _currentElement?.widget;
State get currentState { State get currentState {
Element element = _registry[this]; Element element = _currentElement;
if (element is StatefulComponentElement) if (element is StatefulComponentElement)
return element.state; return element.state;
return null; return null;
...@@ -416,26 +417,26 @@ enum _ElementLifecycle { ...@@ -416,26 +417,26 @@ enum _ElementLifecycle {
defunct, defunct,
} }
class _FreeElements { class _InactiveElements {
bool _locked = false; bool _locked = false;
final Set<Element> _elements = new Set<Element>(); final Set<Element> _elements = new Set<Element>();
void _finalize(Element element) { void _unmount(Element element) {
assert(element._debugLifecycleState == _ElementLifecycle.inactive); assert(element._debugLifecycleState == _ElementLifecycle.inactive);
element.unmount(); element.unmount();
assert(element._debugLifecycleState == _ElementLifecycle.defunct); assert(element._debugLifecycleState == _ElementLifecycle.defunct);
element.visitChildren((Element child) { element.visitChildren((Element child) {
assert(child._parent == element); assert(child._parent == element);
_finalize(child); _unmount(child);
}); });
} }
void finalizeAll() { void unmountAll() {
BuildableElement.lockState(() { BuildableElement.lockState(() {
try { try {
_locked = true; _locked = true;
for (Element element in _elements) for (Element element in _elements)
_finalize(element); _unmount(element);
} finally { } finally {
_elements.clear(); _elements.clear();
_locked = false; _locked = false;
...@@ -443,22 +444,40 @@ class _FreeElements { ...@@ -443,22 +444,40 @@ class _FreeElements {
}); });
} }
void _deactivate(Element element) {
assert(element._debugLifecycleState == _ElementLifecycle.active);
element.deactivate();
assert(element._debugLifecycleState == _ElementLifecycle.inactive);
element.visitChildren(_deactivate);
}
void add(Element element) { void add(Element element) {
assert(!_locked); assert(!_locked);
assert(!_elements.contains(element)); assert(!_elements.contains(element));
assert(element._parent == null); assert(element._parent == null);
void deactivate(Element element) { if (element._active)
assert(element._debugLifecycleState == _ElementLifecycle.active); _deactivate(element);
element.deactivate();
assert(element._debugLifecycleState == _ElementLifecycle.inactive);
element.visitChildren(deactivate);
}
deactivate(element);
_elements.add(element); _elements.add(element);
} }
void _reactivate(Element element) {
assert(element._debugLifecycleState == _ElementLifecycle.inactive);
element.reactivate();
assert(element._debugLifecycleState == _ElementLifecycle.active);
element.visitChildren(_reactivate);
}
void remove(Element element) {
assert(!_locked);
assert(_elements.contains(element));
assert(element._parent == null);
_elements.remove(element);
assert(!element._active);
_reactivate(element);
}
} }
final _FreeElements _freeElements = new _FreeElements(); final _InactiveElements _inactiveElements = new _InactiveElements();
typedef void ElementVisitor(Element element); typedef void ElementVisitor(Element element);
...@@ -496,6 +515,8 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -496,6 +515,8 @@ abstract class Element<T extends Widget> implements BuildContext {
T get widget => _widget; T get widget => _widget;
T _widget; T _widget;
bool _active = false;
RenderObject get renderObject { RenderObject get renderObject {
RenderObject result; RenderObject result;
void visit(Element element) { void visit(Element element) {
...@@ -515,6 +536,8 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -515,6 +536,8 @@ abstract class Element<T extends Widget> implements BuildContext {
/// Calls the argument for each child. Must be overridden by subclasses that support having children. /// Calls the argument for each child. Must be overridden by subclasses that support having children.
void visitChildren(ElementVisitor visitor) { } void visitChildren(ElementVisitor visitor) { }
bool detachChild(Element child) => false;
/// This method is the core of the system. /// This method is the core of the system.
/// ///
/// It is called each time we are to add, update, or remove a child based on /// It is called each time we are to add, update, or remove a child based on
...@@ -540,7 +563,7 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -540,7 +563,7 @@ abstract class Element<T extends Widget> implements BuildContext {
Element updateChild(Element child, Widget newWidget, dynamic newSlot) { Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) { if (newWidget == null) {
if (child != null) if (child != null)
_detachChild(child); _deactivateChild(child);
return null; return null;
} }
if (child != null) { if (child != null) {
...@@ -556,17 +579,14 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -556,17 +579,14 @@ abstract class Element<T extends Widget> implements BuildContext {
assert(child.widget == newWidget); assert(child.widget == newWidget);
return child; return child;
} }
_detachChild(child); _deactivateChild(child);
assert(child._parent == null); assert(child._parent == null);
} }
child = newWidget.createElement(); return _inflateWidget(newWidget, newSlot);
child.mount(this, newSlot);
assert(child._debugLifecycleState == _ElementLifecycle.active);
return child;
} }
static void finalizeTree() { static void finalizeTree() {
_freeElements.finalizeAll(); _inactiveElements.unmountAll();
assert(GlobalKey._debugCheckForDuplicates); assert(GlobalKey._debugCheckForDuplicates);
scheduleMicrotask(GlobalKey._notifyListeners); scheduleMicrotask(GlobalKey._notifyListeners);
} }
...@@ -582,9 +602,11 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -582,9 +602,11 @@ abstract class Element<T extends Widget> implements BuildContext {
assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active); assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active);
assert(slot == null); assert(slot == null);
assert(depth == null); assert(depth == null);
assert(!_active);
_parent = parent; _parent = parent;
_slot = newSlot; _slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1; _depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (widget.key is GlobalKey) { if (widget.key is GlobalKey) {
final GlobalKey key = widget.key; final GlobalKey key = widget.key;
key._register(this); key._register(this);
...@@ -598,6 +620,7 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -598,6 +620,7 @@ abstract class Element<T extends Widget> implements BuildContext {
assert(widget != null); assert(widget != null);
assert(newWidget != null); assert(newWidget != null);
assert(depth != null); assert(depth != null);
assert(_active);
assert(_canUpdate(widget, newWidget)); assert(_canUpdate(widget, newWidget));
_widget = newWidget; _widget = newWidget;
} }
...@@ -626,6 +649,16 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -626,6 +649,16 @@ abstract class Element<T extends Widget> implements BuildContext {
_slot = newSlot; _slot = newSlot;
} }
void _updateDepth() {
int expectedDepth = _parent.depth + 1;
if (_depth < expectedDepth) {
_depth = expectedDepth;
visitChildren((Element child) {
child._updateDepth();
});
}
}
void detachRenderObject() { void detachRenderObject() {
visitChildren((Element child) { visitChildren((Element child) {
child.detachRenderObject(); child.detachRenderObject();
...@@ -633,27 +666,80 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -633,27 +666,80 @@ abstract class Element<T extends Widget> implements BuildContext {
_slot = null; _slot = null;
} }
void _detachChild(Element child) { void attachRenderObject(dynamic newSlot) {
assert(_slot == null);
visitChildren((Element child) {
child.attachRenderObject(newSlot);
});
_slot = newSlot;
}
Element _findAndActivateElement(GlobalKey key, Widget newWidget) {
Element element = key._currentElement;
if (element == null)
return null;
if (!_canUpdate(element.widget, newWidget))
return null;
if (element._parent != null && !element._parent.detachChild(element))
return null;
assert(element._parent == null);
_inactiveElements.remove(element);
return element;
}
Element _inflateWidget(Widget newWidget, dynamic newSlot) {
Key key = newWidget.key;
if (key is GlobalKey) {
Element newChild = _findAndActivateElement(key, newWidget);
if (newChild != null) {
assert(newChild._parent == null);
newChild._parent = this;
newChild._updateDepth();
newChild.attachRenderObject(newSlot);
Element updatedChild = updateChild(newChild, newWidget, newSlot);
assert(newChild == updatedChild);
return updatedChild;
}
}
Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
return newChild;
}
void _deactivateChild(Element child) {
assert(child != null); assert(child != null);
assert(child._parent == this); assert(child._parent == this);
child._parent = null; child._parent = null;
child.detachRenderObject(); child.detachRenderObject();
_freeElements.add(child); _inactiveElements.add(child);
} }
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);
_active = false;
assert(() { _debugLifecycleState = _ElementLifecycle.inactive; return true; }); assert(() { _debugLifecycleState = _ElementLifecycle.inactive; return true; });
} }
void reactivate() {
assert(_debugLifecycleState == _ElementLifecycle.inactive);
assert(widget != null);
assert(depth != null);
assert(!_active);
_active = true;
assert(() { _debugLifecycleState = _ElementLifecycle.active; return true; });
}
/// Called when an Element is removed from the tree. /// Called when an Element is removed from the tree.
/// Currently, an Element removed from the tree never returns. /// Currently, an Element removed from the tree never returns.
void unmount() { void unmount() {
assert(_debugLifecycleState == _ElementLifecycle.inactive); assert(_debugLifecycleState == _ElementLifecycle.inactive);
assert(widget != null); assert(widget != null);
assert(depth != null); assert(depth != null);
assert(!_active);
if (widget.key is GlobalKey) { if (widget.key is GlobalKey) {
final GlobalKey key = widget.key; final GlobalKey key = widget.key;
key._unregister(this); key._unregister(this);
...@@ -733,8 +819,6 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -733,8 +819,6 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
bool get dirty => _dirty; bool get dirty => _dirty;
bool _dirty = true; bool _dirty = true;
bool _active = false;
// We to let component authors call setState from initState, didUpdateConfig, // We to let component authors call setState from initState, didUpdateConfig,
// and build even when state is locked because its convenient and a no-op // and build even when state is locked because its convenient and a no-op
// anyway. This flag ensures that this convenience is only allowed on the // anyway. This flag ensures that this convenience is only allowed on the
...@@ -749,8 +833,7 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -749,8 +833,7 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
void mount(Element parent, dynamic newSlot) { void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot); super.mount(parent, newSlot);
assert(_child == null); assert(_child == null);
assert(!_active); assert(_active);
_active = true;
rebuild(); rebuild();
assert(_child != null); assert(_child != null);
} }
...@@ -845,10 +928,11 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -845,10 +928,11 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
visitor(_child); visitor(_child);
} }
void deactivate() { bool detachChild(Element child) {
super.deactivate(); assert(child == _child);
assert(_active == true); _deactivateChild(_child);
_active = false; _child = null;
return true;
} }
void dependenciesChanged() { void dependenciesChanged() {
...@@ -1039,9 +1123,8 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element ...@@ -1039,9 +1123,8 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element
void mount(Element parent, dynamic newSlot) { void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot); super.mount(parent, newSlot);
assert(_ancestorRenderObjectElement == null); assert(_slot == newSlot);
_ancestorRenderObjectElement = _findAncestorRenderObjectElement(); attachRenderObject(newSlot);
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
ParentDataElement parentDataElement = _findAncestorParentDataElement(); ParentDataElement parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null) if (parentDataElement != null)
updateParentData(parentDataElement.widget); updateParentData(parentDataElement.widget);
...@@ -1141,7 +1224,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element ...@@ -1141,7 +1224,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element
if (oldChild.widget.key != null) if (oldChild.widget.key != null)
oldKeyedChildren[oldChild.widget.key] = oldChild; oldKeyedChildren[oldChild.widget.key] = oldChild;
else else
_detachChild(oldChild); _deactivateChild(oldChild);
oldChildrenBottom -= 1; oldChildrenBottom -= 1;
} }
} }
...@@ -1194,7 +1277,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element ...@@ -1194,7 +1277,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element
// clean up any of the remaining middle nodes from the old list // clean up any of the remaining middle nodes from the old list
if (haveOldNodes && !oldKeyedChildren.isEmpty) { if (haveOldNodes && !oldKeyedChildren.isEmpty) {
for (Element oldChild in oldKeyedChildren.values) for (Element oldChild in oldKeyedChildren.values)
_detachChild(oldChild); _deactivateChild(oldChild);
} }
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
...@@ -1223,6 +1306,13 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element ...@@ -1223,6 +1306,13 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element
_ancestorRenderObjectElement.moveChildRenderObject(renderObject, slot); _ancestorRenderObjectElement.moveChildRenderObject(renderObject, slot);
} }
void attachRenderObject(dynamic newSlot) {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
}
void detachRenderObject() { void detachRenderObject() {
if (_ancestorRenderObjectElement != null) { if (_ancestorRenderObjectElement != null) {
_ancestorRenderObjectElement.removeChildRenderObject(renderObject); _ancestorRenderObjectElement.removeChildRenderObject(renderObject);
...@@ -1270,6 +1360,13 @@ class OneChildRenderObjectElement<T extends OneChildRenderObjectWidget> extends ...@@ -1270,6 +1360,13 @@ class OneChildRenderObjectElement<T extends OneChildRenderObjectWidget> extends
visitor(_child); visitor(_child);
} }
bool detachChild(Element child) {
assert(child == _child);
_deactivateChild(_child);
_child = null;
return true;
}
void mount(Element parent, dynamic newSlot) { void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot); super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null); _child = updateChild(_child, widget.child, null);
...@@ -1358,9 +1455,7 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte ...@@ -1358,9 +1455,7 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte
_children = new List<Element>(widget.children.length); _children = new List<Element>(widget.children.length);
Element previousChild; Element previousChild;
for (int i = _children.length - 1; i >= 0; --i) { for (int i = _children.length - 1; i >= 0; --i) {
Element newChild = widget.children[i].createElement(); Element newChild = _inflateWidget(widget.children[i], previousChild);
newChild.mount(this, previousChild);
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
_children[i] = newChild; _children[i] = newChild;
previousChild = newChild; previousChild = newChild;
} }
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:sky/widgets.dart';
import 'package:test/test.dart';
import 'widget_tester.dart';
class StateMarker extends StatefulComponent {
StateMarker({ Key key, this.child }) : super(key: key);
final Widget child;
StateMarkerState createState() => new StateMarkerState();
}
class StateMarkerState extends State<StateMarker> {
String marker;
Widget build(BuildContext context) {
if (config.child != null)
return config.child;
return new Container();
}
}
void main() {
test('can reparent state', () {
testWidgets((WidgetTester tester) {
GlobalKey left = new GlobalKey();
GlobalKey right = new GlobalKey();
StateMarker grandchild = new StateMarker();
tester.pumpWidget(
new Stack([
new Container(
child: new StateMarker(key: left)
),
new Container(
child: new StateMarker(
key: right,
child: grandchild
)
),
])
);
(left.currentState as StateMarkerState).marker = "left";
(right.currentState as StateMarkerState).marker = "right";
StateMarkerState grandchildState = tester.findStateByConfig(grandchild);
expect(grandchildState, isNotNull);
grandchildState.marker = "grandchild";
StateMarker newGrandchild = new StateMarker();
tester.pumpWidget(
new Stack([
new Container(
child: new StateMarker(
key: right,
child: newGrandchild
)
),
new Container(
child: new StateMarker(key: left)
),
])
);
expect((left.currentState as StateMarkerState).marker, equals("left"));
expect((right.currentState as StateMarkerState).marker, equals("right"));
StateMarkerState newGrandchildState = tester.findStateByConfig(newGrandchild);
expect(newGrandchildState, isNotNull);
expect(newGrandchildState, equals(grandchildState));
expect(newGrandchildState.marker, equals("grandchild"));
tester.pumpWidget(
new Center(
child: new Container(
child: new StateMarker(
key: left,
child: new Container()
)
)
)
);
expect((left.currentState as StateMarkerState).marker, equals("left"));
expect(right.currentState, isNull);
});
});
test('can with scrollable list', () {
testWidgets((WidgetTester tester) {
GlobalKey key = new GlobalKey();
tester.pumpWidget(new StateMarker(key: key));
(key.currentState as StateMarkerState).marker = "marked";
tester.pumpWidget(new ScrollableList<int>(
items: [0],
itemExtent: 100.0,
itemBuilder: (BuildContext context, int item) {
return new Container(
key: new Key('container'),
height: 100.0,
child: new StateMarker(key: key)
);
}
));
expect((key.currentState as StateMarkerState).marker, equals("marked"));
tester.pumpWidget(new StateMarker(key: key));
expect((key.currentState as StateMarkerState).marker, equals("marked"));
});
});
}
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