Commit dfd821e5 authored by Hixie's avatar Hixie

Ignore generation of child if child is unchanged

Also:

 - don't mark a node as from the new generation if it is dirty, since we
   know it still has to be built.

 - establish the rule that you can't call setState() during initState()
   or build().

 - make syncChild() return early for unchanged children.

 - update the tests, including adding a new one.
parent 2cb58ebf
...@@ -389,18 +389,12 @@ abstract class Widget { ...@@ -389,18 +389,12 @@ abstract class Widget {
// Returns the child which should be retained as the child of this node. // Returns the child which should be retained as the child of this node.
Widget syncChild(Widget newNode, Widget oldNode, dynamic slot) { Widget syncChild(Widget newNode, Widget oldNode, dynamic slot) {
assert(() {
'You have probably used a single instance of a Widget in two different places in the widget tree. Widgets can only be used in one place at a time.';
return newNode == null || newNode.isFromOldGeneration;
});
if (oldNode != null && !oldNode.isFromOldGeneration)
oldNode = null;
if (newNode == oldNode) { if (newNode == oldNode) {
assert(newNode == null || newNode.isFromOldGeneration); // TODO(ianh): Simplify next few asserts once the analyzer is cleverer
assert(newNode is! RenderObjectWrapper || assert(newNode is! RenderObjectWrapper ||
(newNode is RenderObjectWrapper && newNode._ancestor != null)); // TODO(ianh): Simplify this once the analyzer is cleverer (newNode is RenderObjectWrapper && newNode._ancestor != null)); // if the child didn't change, it had better be configured
assert(newNode is! StatefulComponent ||
(newNode is StatefulComponent && newNode._isStateInitialized)); // if the child didn't change, it had better be configured
if (newNode != null) { if (newNode != null) {
newNode.setParent(this); newNode.setParent(this);
newNode._markAsFromCurrentGeneration(); newNode._markAsFromCurrentGeneration();
...@@ -408,8 +402,18 @@ abstract class Widget { ...@@ -408,8 +402,18 @@ abstract class Widget {
return newNode; // Nothing to do. Subtrees must be identical. return newNode; // Nothing to do. Subtrees must be identical.
} }
assert(() {
'You have probably used a single instance of a Widget in two different places in the widget tree. Widgets can only be used in one place at a time.';
return newNode == null || newNode.isFromOldGeneration;
});
if (oldNode != null && !oldNode.isFromOldGeneration)
oldNode = null;
if (newNode == null) { if (newNode == null) {
// the child in this slot has gone away (we know oldNode != null) // the child in this slot has gone away
// remove it if they old one is still here
if (oldNode != null) {
assert(oldNode != null); assert(oldNode != null);
assert(oldNode.isFromOldGeneration); assert(oldNode.isFromOldGeneration);
assert(oldNode.mounted); assert(oldNode.mounted);
...@@ -418,6 +422,7 @@ abstract class Widget { ...@@ -418,6 +422,7 @@ abstract class Widget {
assert(!oldNode.mounted); assert(!oldNode.mounted);
// we don't update the generation of oldNode, because there's // we don't update the generation of oldNode, because there's
// still a chance it could be reused as-is later in the tree. // still a chance it could be reused as-is later in the tree.
}
return null; return null;
} }
...@@ -868,6 +873,12 @@ abstract class Component extends Widget { ...@@ -868,6 +873,12 @@ abstract class Component extends Widget {
super._sync(old, slot); super._sync(old, slot);
} }
void _markAsFromCurrentGeneration() {
if (_dirty)
return;
super._markAsFromCurrentGeneration();
}
void _buildIfDirty() { void _buildIfDirty() {
if (!_dirty || !_mounted) if (!_dirty || !_mounted)
return; return;
...@@ -947,6 +958,8 @@ abstract class StatefulComponent extends Component { ...@@ -947,6 +958,8 @@ abstract class StatefulComponent extends Component {
// Calls function fn immediately and then schedules another build // Calls function fn immediately and then schedules another build
// for this Component. // for this Component.
void setState(void fn()) { void setState(void fn()) {
assert(!_debugIsBuilding);
assert(_isStateInitialized);
fn(); fn();
_scheduleBuild(); _scheduleBuild();
} }
...@@ -985,12 +998,14 @@ void exitLayoutCallbackBuilder(LayoutCallbackBuilderHandle handle) { ...@@ -985,12 +998,14 @@ void exitLayoutCallbackBuilder(LayoutCallbackBuilderHandle handle) {
List<int> _debugFrameTimes = <int>[]; List<int> _debugFrameTimes = <int>[];
// TODO(ianh): Move this to Component
void _absorbDirtyComponents(List<Component> list) { void _absorbDirtyComponents(List<Component> list) {
list.addAll(_dirtyComponents); list.addAll(_dirtyComponents);
_dirtyComponents.clear(); _dirtyComponents.clear();
list.sort((Component a, Component b) => a._order - b._order); list.sort((Component a, Component b) => a._order - b._order);
} }
// TODO(ianh): Move this to Component
void _buildDirtyComponents() { void _buildDirtyComponents() {
assert(!_dirtyComponents.isEmpty); assert(!_dirtyComponents.isEmpty);
...@@ -1038,11 +1053,13 @@ void _buildDirtyComponents() { ...@@ -1038,11 +1053,13 @@ void _buildDirtyComponents() {
} }
// TODO(ianh): Move this to Widget
void _endSyncPhase() { void _endSyncPhase() {
Widget._currentGeneration += 1; Widget._currentGeneration += 1;
Widget._notifyMountStatusChanged(); Widget._notifyMountStatusChanged();
} }
// TODO(ianh): Move this to Component
void _scheduleComponentForRender(Component component) { void _scheduleComponentForRender(Component component) {
_dirtyComponents.add(component); _dirtyComponents.add(component);
if (!_buildScheduled) { if (!_buildScheduled) {
......
...@@ -4,50 +4,50 @@ import 'package:test/test.dart'; ...@@ -4,50 +4,50 @@ import 'package:test/test.dart';
import 'widget_tester.dart'; import 'widget_tester.dart';
class SecondComponent extends StatefulComponent { class FirstComponent extends Component {
SecondComponent(this.navigator); FirstComponent(this.navigator);
Navigator navigator;
void syncConstructorArguments(SecondComponent source) { final Navigator navigator;
navigator = source.navigator;
}
Widget build() { Widget build() {
return new GestureDetector( return new GestureDetector(
onTap: navigator.pop, onTap: () {
navigator.pushNamed('/second');
},
child: new Container( child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: new Color(0xFFFF00FF) backgroundColor: new Color(0xFFFFFF00)
), ),
child: new Text('Y') child: new Text('X')
) )
); );
} }
} }
class FirstComponent extends Component { class SecondComponent extends StatefulComponent {
FirstComponent(this.navigator); SecondComponent(this.navigator);
final Navigator navigator; Navigator navigator;
void syncConstructorArguments(SecondComponent source) {
navigator = source.navigator;
}
Widget build() { Widget build() {
return new GestureDetector( return new GestureDetector(
onTap: () { onTap: navigator.pop,
navigator.pushNamed('/second');
},
child: new Container( child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: new Color(0xFFFFFF00) backgroundColor: new Color(0xFFFF00FF)
), ),
child: new Text('X') child: new Text('Y')
) )
); );
} }
} }
void main() { void main() {
test('Can navigator to and from a stateful component', () { test('Can navigator navigate to and from a stateful component', () {
WidgetTester tester = new WidgetTester(); WidgetTester tester = new WidgetTester();
final NavigationState routes = new NavigationState([ final NavigationState routes = new NavigationState([
...@@ -65,8 +65,15 @@ void main() { ...@@ -65,8 +65,15 @@ void main() {
return new Navigator(routes); return new Navigator(routes);
}); });
expect(tester.findText('X'), isNotNull);
expect(tester.findText('Y'), isNull);
tester.tap(tester.findText('X')); tester.tap(tester.findText('X'));
scheduler.beginFrame(10.0); scheduler.beginFrame(10.0);
expect(tester.findText('X'), isNotNull);
expect(tester.findText('Y'), isNotNull);
scheduler.beginFrame(20.0); scheduler.beginFrame(20.0);
scheduler.beginFrame(30.0); scheduler.beginFrame(30.0);
scheduler.beginFrame(1000.0); scheduler.beginFrame(1000.0);
...@@ -77,5 +84,8 @@ void main() { ...@@ -77,5 +84,8 @@ void main() {
scheduler.beginFrame(1030.0); scheduler.beginFrame(1030.0);
scheduler.beginFrame(2000.0); scheduler.beginFrame(2000.0);
expect(tester.findText('X'), isNotNull);
expect(tester.findText('Y'), isNull);
}); });
} }
import 'package:sky/gestures/arena.dart';
import 'package:sky/gestures/pointer_router.dart';
import 'package:sky/gestures/tap.dart';
import 'package:sky/widgets.dart';
import 'package:test/test.dart';
import '../engine/mock_events.dart';
import 'widget_tester.dart';
class Inside extends StatefulComponent {
void syncConstructorArguments(Inside source) {
}
Widget build() {
return new Listener(
onPointerDown: _handlePointerDown,
child: new Text('INSIDE')
);
}
EventDisposition _handlePointerDown(_) {
setState(() { });
return EventDisposition.processed;
}
}
class Middle extends StatefulComponent {
Inside child;
Middle({ this.child });
void syncConstructorArguments(Middle source) {
child = source.child;
}
Widget build() {
return new Listener(
onPointerDown: _handlePointerDown,
child: child
);
}
EventDisposition _handlePointerDown(_) {
setState(() { });
return EventDisposition.processed;
}
}
class Outside extends App {
Widget build() {
return new Middle(child: new Inside());
}
}
void main() {
test('setState() smoke test', () {
WidgetTester tester = new WidgetTester();
tester.pumpFrame(() {
return new Outside();
});
TestPointer pointer = new TestPointer(1);
Point location = tester.getCenter(tester.findText('INSIDE'));
tester.dispatchEvent(pointer.down(location), location);
tester.pumpFrameWithoutChange();
tester.dispatchEvent(pointer.up(), location);
tester.pumpFrameWithoutChange();
});
}
\ No newline at end of file
...@@ -40,27 +40,36 @@ void main() { ...@@ -40,27 +40,36 @@ void main() {
WidgetTester tester = new WidgetTester(); WidgetTester tester = new WidgetTester();
InnerComponent inner; InnerComponent inner1;
InnerComponent inner2;
OuterContainer outer; OuterContainer outer;
tester.pumpFrame(() { tester.pumpFrame(() {
return new OuterContainer(child: new InnerComponent()); inner1 = new InnerComponent();
outer = new OuterContainer(child: inner1);
return outer;
}); });
expect(inner1._didInitState, isTrue);
expect(inner1.parent, isNotNull);
tester.pumpFrame(() { tester.pumpFrame(() {
inner = new InnerComponent(); inner2 = new InnerComponent();
outer = new OuterContainer(child: inner); return new OuterContainer(child: inner2);
return outer;
}); });
expect(inner._didInitState, isFalse); expect(inner1._didInitState, isTrue);
expect(inner.parent, isNull); expect(inner1.parent, isNotNull);
expect(inner2._didInitState, isFalse);
expect(inner2.parent, isNull);
outer.setState(() {}); outer.setState(() {});
scheduler.beginFrame(0.0); tester.pumpFrameWithoutChange(0.0);
expect(inner._didInitState, isFalse); expect(inner1._didInitState, isTrue);
expect(inner.parent, isNull); expect(inner1.parent, isNotNull);
expect(inner2._didInitState, isFalse);
expect(inner2.parent, isNull);
}); });
} }
...@@ -9,7 +9,6 @@ import '../engine/mock_events.dart'; ...@@ -9,7 +9,6 @@ import '../engine/mock_events.dart';
typedef Widget WidgetBuilder(); typedef Widget WidgetBuilder();
class TestApp extends App { class TestApp extends App {
TestApp();
WidgetBuilder _builder; WidgetBuilder _builder;
void set builder (WidgetBuilder value) { void set builder (WidgetBuilder value) {
...@@ -29,6 +28,7 @@ class WidgetTester { ...@@ -29,6 +28,7 @@ class WidgetTester {
WidgetTester() { WidgetTester() {
_app = new TestApp(); _app = new TestApp();
runApp(_app); runApp(_app);
scheduler.beginFrame(0.0); // to initialise the app
} }
TestApp _app; TestApp _app;
......
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