Commit 64dfb849 authored by Adam Barth's avatar Adam Barth

Actually notify GlobalKey listeners in fn3

This patch makes a number of changes:

1) buildDirtyComponents now prevents all calls to setState, not just those
   higher in the tree. This change is necessary for consistency with
   MixedViewport and HomogeneousViewport because those widgets already build
   subwidgets with that restriction. If the "normal" build didn't enforce that
   rule, then some widgets would break when put inside a mixed or homogeneous
   viewport.

2) We now notify global key listeners in a microtask after beginFrame. That
   means setState is legal in these callbacks and that we'll produce another
   frame if someone calls setState in such a callback.
parent f9b5b145
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:sky/animation.dart'; import 'package:sky/animation.dart';
import 'package:sky/rendering.dart'; import 'package:sky/rendering.dart';
import 'package:sky/src/fn3/framework.dart'; import 'package:sky/src/fn3/framework.dart';
...@@ -15,7 +17,7 @@ class WidgetFlutterBinding extends FlutterBinding { ...@@ -15,7 +17,7 @@ class WidgetFlutterBinding extends FlutterBinding {
} }
/// Ensures that there is a FlutterBinding object instantiated. /// Ensures that there is a FlutterBinding object instantiated.
static void initBinding() { static void ensureInitialized() {
if (FlutterBinding.instance == null) if (FlutterBinding.instance == null)
new WidgetFlutterBinding(); new WidgetFlutterBinding();
assert(FlutterBinding.instance is WidgetFlutterBinding); assert(FlutterBinding.instance is WidgetFlutterBinding);
...@@ -38,16 +40,14 @@ class WidgetFlutterBinding extends FlutterBinding { ...@@ -38,16 +40,14 @@ class WidgetFlutterBinding extends FlutterBinding {
void beginFrame(double timeStamp) { void beginFrame(double timeStamp) {
buildDirtyElements(); buildDirtyElements();
super.beginFrame(timeStamp); super.beginFrame(timeStamp);
scheduleMicrotask(GlobalKey.checkForDuplicatesAndNotifyListeners);
} }
final List<BuildableElement> _dirtyElements = new List<BuildableElement>(); List<BuildableElement> _dirtyElements = new List<BuildableElement>();
int _debugBuildingAtDepth;
/// Adds an element to the dirty elements list so that it will be rebuilt /// Adds an element to the dirty elements list so that it will be rebuilt
/// when buildDirtyElements is called. /// when buildDirtyElements is called.
void scheduleBuildFor(BuildableElement element) { void scheduleBuildFor(BuildableElement element) {
assert(_debugBuildingAtDepth == null || element.depth > _debugBuildingAtDepth);
assert(!_dirtyElements.contains(element)); assert(!_dirtyElements.contains(element));
assert(element.dirty); assert(element.dirty);
if (_dirtyElements.isEmpty) if (_dirtyElements.isEmpty)
...@@ -55,46 +55,30 @@ class WidgetFlutterBinding extends FlutterBinding { ...@@ -55,46 +55,30 @@ class WidgetFlutterBinding extends FlutterBinding {
_dirtyElements.add(element); _dirtyElements.add(element);
} }
void _absorbDirtyElements(List<BuildableElement> list) {
assert(_debugBuildingAtDepth != null);
assert(!_dirtyElements.any((element) => element.depth <= _debugBuildingAtDepth));
_dirtyElements.sort((BuildableElement a, BuildableElement b) => a.depth - b.depth);
list.addAll(_dirtyElements);
_dirtyElements.clear();
}
/// Builds all the elements that were marked as dirty using schedule(), in depth order. /// Builds all the elements that were marked as dirty using schedule(), in depth order.
/// If elements are marked as dirty while this runs, they must be deeper than the algorithm /// If elements are marked as dirty while this runs, they must be deeper than the algorithm
/// has yet reached. /// has yet reached.
/// This is called by beginFrame(). /// This is called by beginFrame().
void buildDirtyElements() { void buildDirtyElements() {
assert(_debugBuildingAtDepth == null);
if (_dirtyElements.isEmpty) if (_dirtyElements.isEmpty)
return; return;
assert(() { _debugBuildingAtDepth = 0; return true; }); BuildableElement.lockState(() {
List<BuildableElement> sortedDirtyElements = new List<BuildableElement>(); _dirtyElements.sort((BuildableElement a, BuildableElement b) => a.depth - b.depth);
int index = 0; for (BuildableElement element in _dirtyElements)
do {
_absorbDirtyElements(sortedDirtyElements);
for (; index < sortedDirtyElements.length; index += 1) {
BuildableElement element = sortedDirtyElements[index];
assert(() {
if (element.depth > _debugBuildingAtDepth)
_debugBuildingAtDepth = element.depth;
return element.depth == _debugBuildingAtDepth;
});
element.rebuild(); element.rebuild();
} _dirtyElements.clear();
} while (_dirtyElements.isNotEmpty); });
assert(() { _debugBuildingAtDepth = null; return true; }); assert(_dirtyElements.isEmpty);
} }
} }
void runApp(Widget app) { void runApp(Widget app) {
WidgetFlutterBinding.initBinding(); WidgetFlutterBinding.ensureInitialized();
WidgetFlutterBinding.instance.renderViewElement.update( BuildableElement.lockState(() {
WidgetFlutterBinding.instance.describeApp(app) WidgetFlutterBinding.instance.renderViewElement.update(
); WidgetFlutterBinding.instance.describeApp(app)
);
});
} }
/// This class provides a bridge from a RenderObject to an Element tree. The /// This class provides a bridge from a RenderObject to an Element tree. The
......
...@@ -118,8 +118,7 @@ abstract class GlobalKey extends Key { ...@@ -118,8 +118,7 @@ abstract class GlobalKey extends Key {
assert(removed); assert(removed);
} }
// TODO(ianh): call this static void checkForDuplicatesAndNotifyListeners() {
static void _notifyListeners() {
assert(() { assert(() {
String message = ''; String message = '';
for (GlobalKey key in _debugDuplicates.keys) { for (GlobalKey key in _debugDuplicates.keys) {
...@@ -326,12 +325,7 @@ abstract class State<T extends StatefulComponent> { ...@@ -326,12 +325,7 @@ abstract class State<T extends StatefulComponent> {
void setState(void fn()) { void setState(void fn()) {
assert(_debugLifecycleState != _StateLifecycle.defunct); assert(_debugLifecycleState != _StateLifecycle.defunct);
fn(); fn();
if (_element._builder != null) { _element.markNeedsBuild();
// _element._builder is set after initState(). We verify that we're past
// that before calling markNeedsBuild() so that setState()s triggered
// during initState() during lockState() don't cause any trouble.
_element.markNeedsBuild();
}
} }
/// Called when this object is removed from the tree. Override this to clean /// Called when this object is removed from the tree. Override this to clean
...@@ -645,6 +639,17 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -645,6 +639,17 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
bool get dirty => _dirty; bool get dirty => _dirty;
bool _dirty = true; bool _dirty = true;
// We to let component authors call setState from initState, didUpdateConfig,
// 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
// element currently undergoing initState, didUpdateConfig, or build.
bool _debugAllowIgnoredCallsToMarkNeedsBuild = false;
bool _debugSetAllowIgnoredCallsToMarkNeedsBuild(bool value) {
assert(_debugAllowIgnoredCallsToMarkNeedsBuild == !value);
_debugAllowIgnoredCallsToMarkNeedsBuild = value;
return true;
}
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);
...@@ -664,21 +669,27 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -664,21 +669,27 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
if (!_dirty) if (!_dirty)
return; return;
assert(_debugLifecycleState == _ElementLifecycle.mounted); assert(_debugLifecycleState == _ElementLifecycle.mounted);
_dirty = false; assert(_debugStateLocked);
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
Widget built; Widget built;
try { try {
built = _builder(this); built = _builder(this);
assert(built != null); assert(built != null);
} catch (e, stack) { } catch (e, stack) {
_debugReportException('building $this', e, stack); _debugReportException('building ${_widget}', e, stack);
built = new ErrorWidget(); built = new ErrorWidget();
} finally {
// We delay marking the element as clean until after calling _builder so
// that attempts to markNeedsBuild() during build() will be ignored.
_dirty = false;
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
} }
try { try {
_child = updateChild(_child, built, slot); _child = updateChild(_child, built, slot);
assert(_child != null); assert(_child != null);
} catch (e, stack) { } catch (e, stack) {
_debugReportException('building $this', e, stack); _debugReportException('building ${_widget}', e, stack);
built = new ErrorWidget(); built = new ErrorWidget();
_child = updateChild(null, built, slot); _child = updateChild(null, built, slot);
} }
...@@ -689,12 +700,16 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -689,12 +700,16 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
static int _debugStateLockLevel = 0; static int _debugStateLockLevel = 0;
static bool get _debugStateLocked => _debugStateLockLevel > 0; static bool get _debugStateLocked => _debugStateLockLevel > 0;
/// Calls the callback argument synchronously, but in a context where calls to /// Establishes a scope in which component build functions can run.
/// State.setState() will fail. Use this when it is possible that you will ///
/// trigger code in components but want to make sure that there is no /// Inside a build scope, component build functions are allowed to run, but
/// possibility that any components will be marked dirty, for example because /// State.setState() is forbidden. This mechanism prevents build functions
/// you are in the middle of layout and you are not going to be flushing the /// from transitively requiring other build functions to run, potentially
/// build queue (since that could mutate the layout tree). /// causing infinite loops.
///
/// After unwinding the last build scope on the stack, the framework verifies
/// that each global key is used at most once and notifies listeners about
/// changes to global keys.
static void lockState(void callback()) { static void lockState(void callback()) {
_debugStateLockLevel += 1; _debugStateLockLevel += 1;
try { try {
...@@ -712,10 +727,12 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -712,10 +727,12 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
/// components dirty during event handlers before the frame begins, not during /// components dirty during event handlers before the frame begins, not during
/// the build itself. /// the build itself.
void markNeedsBuild() { void markNeedsBuild() {
assert(!_debugStateLocked); assert(_debugLifecycleState != _ElementLifecycle.defunct);
assert(_debugLifecycleState == _ElementLifecycle.mounted); assert(!_debugStateLocked || (_debugAllowIgnoredCallsToMarkNeedsBuild && _dirty));
if (_dirty) if (_dirty)
return; return;
assert(_debugLifecycleState == _ElementLifecycle.mounted);
assert(!_debugStateLocked);
_dirty = true; _dirty = true;
assert(scheduleBuildFor != null); assert(scheduleBuildFor != null);
scheduleBuildFor(this); scheduleBuildFor(this);
...@@ -757,10 +774,17 @@ class StatefulComponentElement extends BuildableElement<StatefulComponent> { ...@@ -757,10 +774,17 @@ class StatefulComponentElement extends BuildableElement<StatefulComponent> {
: _state = widget.createState(), super(widget) { : _state = widget.createState(), super(widget) {
assert(_state._element == null); assert(_state._element == null);
_state._element = this; _state._element = this;
assert(_builder == null);
_builder = _state.build;
assert(_state._config == null); assert(_state._config == null);
_state._config = widget; _state._config = widget;
assert(_state._debugLifecycleState == _StateLifecycle.created); assert(_state._debugLifecycleState == _StateLifecycle.created);
_state.initState(this); try {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
_state.initState(this);
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
assert(() { assert(() {
if (_state._debugLifecycleState == _StateLifecycle.initialized) if (_state._debugLifecycleState == _StateLifecycle.initialized)
return true; return true;
...@@ -768,10 +792,6 @@ class StatefulComponentElement extends BuildableElement<StatefulComponent> { ...@@ -768,10 +792,6 @@ class StatefulComponentElement extends BuildableElement<StatefulComponent> {
return false; return false;
}); });
assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; }); assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; });
assert(_builder == null);
// see State.setState() for why it's important that _builder be set after
// initState() is called.
_builder = _state.build;
} }
State get state => _state; State get state => _state;
...@@ -781,16 +801,30 @@ class StatefulComponentElement extends BuildableElement<StatefulComponent> { ...@@ -781,16 +801,30 @@ class StatefulComponentElement extends BuildableElement<StatefulComponent> {
super.update(newWidget); super.update(newWidget);
assert(widget == newWidget); assert(widget == newWidget);
StatefulComponent oldConfig = _state._config; StatefulComponent oldConfig = _state._config;
_state._config = widget; // Notice that we mark ourselves as dirty before calling didUpdateConfig to
_state.didUpdateConfig(oldConfig); // let authors call setState from within didUpdateConfig without triggering
// asserts.
_dirty = true; _dirty = true;
_state._config = widget;
try {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
_state.didUpdateConfig(oldConfig);
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
rebuild(); rebuild();
} }
void unmount() { void unmount() {
super.unmount(); super.unmount();
_state.dispose(); _state.dispose();
assert(_state._debugLifecycleState == _StateLifecycle.defunct); assert(() {
if (_state._debugLifecycleState == _StateLifecycle.defunct)
return true;
print('${_state.runtimeType}.dispose failed to call super.dispose');
return false;
});
assert(!_dirty); // See BuildableElement.unmount for why this is important.
_state._element = null; _state._element = null;
_state = null; _state = null;
} }
......
...@@ -90,12 +90,14 @@ class HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewport ...@@ -90,12 +90,14 @@ class HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewport
} }
void layout(BoxConstraints constraints) { void layout(BoxConstraints constraints) {
// we lock the framework state (meaning that no elements can call markNeedsBuild()) because we are // We enter a build scope (meaning that markNeedsBuild() is forbidden)
// in the middle of layout and if we allowed people to set state, they'd expect to have that state // because we are in the middle of layout and if we allowed people to set
// reflected immediately, which, if we were to try to honour it, would potentially result in // state, they'd expect to have that state reflected immediately, which, if
// assertions since you can't normally mutate the render object tree during layout. (If there was // we were to try to honour it, would potentially result in assertions
// a way to limit this to only descendants of this, it'd be ok, since we are exempt from that // because you can't normally mutate the render object tree during layout.
// assert since we are actively doing our own layout still.) // (If there were a way to limit these writes to descendants of this, it'd
// be ok because we are exempt from that assert since we are still actively
// doing our own layout.)
BuildableElement.lockState(() { BuildableElement.lockState(() {
double mainAxisExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth; double mainAxisExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
double offset; double offset;
......
import 'package:sky/src/fn3.dart';
import 'package:test/test.dart';
import '../fn3/widget_tester.dart';
import '../fn3/test_widgets.dart';
class ProbeWidget extends StatefulComponent {
ProbeWidgetState createState() => new ProbeWidgetState();
}
class ProbeWidgetState extends State<ProbeWidget> {
static int buildCount = 0;
void initState(BuildContext context) {
super.initState(context);
setState(() {});
}
void didUpdateConfig(ProbeWidget oldConfig) {
setState(() {});
}
Widget build(BuildContext context) {
setState(() {});
buildCount++;
return new Container();
}
}
class BadWidget extends StatelessComponent {
BadWidget(this.parentState);
final State parentState;
Widget build(BuildContext context) {
parentState.setState(() {});
return new Container();
}
}
class BadWidgetParent extends StatefulComponent {
BadWidgetParentState createState() => new BadWidgetParentState();
}
class BadWidgetParentState extends State<BadWidgetParent> {
Widget build(BuildContext context) {
return new BadWidget(this);
}
}
class BadDisposeWidget extends StatefulComponent {
BadDisposeWidgetState createState() => new BadDisposeWidgetState();
}
class BadDisposeWidgetState extends State<BadDisposeWidget> {
Widget build(BuildContext context) {
return new Container();
}
void dispose() {
setState(() {});
super.dispose();
}
}
void main() {
dynamic cachedException;
setUp(() {
assert(cachedException == null);
debugWidgetsExceptionHandler = (String context, dynamic exception, StackTrace stack) {
cachedException = exception;
};
});
tearDown(() {
assert(cachedException == null);
cachedException = null;
debugWidgetsExceptionHandler = null;
});
test('Legal times for setState', () {
WidgetTester tester = new WidgetTester();
GlobalKey flipKey = new GlobalKey();
expect(ProbeWidgetState.buildCount, equals(0));
tester.pumpFrame(new ProbeWidget());
expect(ProbeWidgetState.buildCount, equals(1));
tester.pumpFrame(new ProbeWidget());
expect(ProbeWidgetState.buildCount, equals(2));
tester.pumpFrame(new FlipComponent(
key: flipKey,
left: new Container(),
right: new ProbeWidget()
));
expect(ProbeWidgetState.buildCount, equals(2));
(flipKey.currentState as FlipComponentState).flip();
tester.pumpFrameWithoutChange();
expect(ProbeWidgetState.buildCount, equals(3));
(flipKey.currentState as FlipComponentState).flip();
tester.pumpFrameWithoutChange();
expect(ProbeWidgetState.buildCount, equals(3));
tester.pumpFrame(new Container());
expect(ProbeWidgetState.buildCount, equals(3));
});
test('Setting parent state during build is forbidden', () {
WidgetTester tester = new WidgetTester();
expect(cachedException, isNull);
tester.pumpFrame(new BadWidgetParent());
expect(cachedException, isNotNull);
cachedException = null;
tester.pumpFrame(new Container());
expect(cachedException, isNull);
});
test('Setting state during dispose is forbidden', () {
WidgetTester tester = new WidgetTester();
tester.pumpFrame(new BadDisposeWidget());
expect(() {
tester.pumpFrame(new Container());
}, throws);
});
}
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