diff --git a/sky/packages/sky/lib/src/fn3/framework.dart b/sky/packages/sky/lib/src/fn3/framework.dart index ae109b96923..71b1493e8bb 100644 --- a/sky/packages/sky/lib/src/fn3/framework.dart +++ b/sky/packages/sky/lib/src/fn3/framework.dart @@ -5,93 +5,149 @@ import 'package:sky/animation.dart'; import 'package:sky/rendering.dart'; -abstract class Key { -} +abstract class Key { } +/// A Widget object describes the configuration for an [Element]. +/// Widget subclasses should be immutable with const constructors. +/// Widgets form a tree that is then inflated into an Element tree. abstract class Widget { - Widget(this.key); + const Widget({ this.key }); final Key key; + /// Inflates this configuration to a concrete instance. Element createElement(); } -typedef Widget WidgetBuilder(); - +/// RenderObjectWidgets provide the configuration for [RenderObjectElement]s, +/// which wrap [RenderObject]s, which provide the actual rendering of the +/// application. abstract class RenderObjectWidget extends Widget { - RenderObjectWidget({ Key key }) : super(key); + const RenderObjectWidget({ Key key }) : super(key: key); - Element createElement() => new RenderObjectElement(this); + /// RenderObjectWidgets always inflate to a RenderObjectElement subclass. + RenderObjectElement createElement(); + /// Constructs an instance of the RenderObject class that this + /// RenderObjectWidget represents, using the configuration described by this + /// RenderObjectWidget. RenderObject createRenderObject(); + + /// Copies the configuration described by this RenderObjectWidget to the given + /// RenderObject, which must be of the same type as returned by this class' + /// createRenderObject(). void updateRenderObject(RenderObject renderObject); } +/// A superclass for RenderObjectWidgets that configure RenderObject subclasses +/// that have no children. +abstract class LeafRenderObjectWidget extends RenderObjectWidget { + const LeafRenderObjectWidget({ Key key }) : super(key: key); + + RenderObjectElement createElement() => new LeafRenderObjectElement(this); +} + +/// A superclass for RenderObjectWidgets that configure RenderObject subclasses +/// that have a single child slot. (This superclass only provides the storage +/// for that child, it doesn't actually provide the updating logic.) abstract class OneChildRenderObjectWidget extends RenderObjectWidget { - OneChildRenderObjectWidget({ Key key, Widget this.child }) : super(key: key); + const OneChildRenderObjectWidget({ Key key, Widget this.child }) : super(key: key); final Widget child; - Element createElement() => new OneChildRenderObjectElement(this); + RenderObjectElement createElement() => new OneChildRenderObjectElement(this); } +/// A superclass for RenderObjectWidgets that configure RenderObject subclasses +/// that have a single list of children. (This superclass only provides the +/// storage for that child list, it doesn't actually provide the updating +/// logic.) abstract class MultiChildRenderObjectWidget extends RenderObjectWidget { - MultiChildRenderObjectWidget({ Key key, List this.children }) + const MultiChildRenderObjectWidget({ Key key, List this.children }) : super(key: key); final List children; - Element createElement() => new MultiChildRenderObjectElement(this); + RenderObjectElement createElement() => new MultiChildRenderObjectElement(this); } -abstract class Component extends Widget { - Component({ Key key }) : super(key); - Element createElement() => new ComponentElement(this); +/// StatelessComponents describe a way to compose other Widgets to form reusable +/// parts, which doesn't depend on anything other than the configuration +/// information in the object itself. (For compositions that can change +/// dynamically, e.g. due to having an internal clock-driven state, or depending +/// on some system state, use [StatefulComponent].) +abstract class StatelessComponent extends Widget { + const StatelessComponent({ Key key }) : super(key: key); + /// StatelessComponents always use StatelessComponentElements to represent + /// themselves in the Element tree. + StatelessComponentElement createElement() => new StatelessComponentElement(this); + + /// Returns another Widget out of which this StatelessComponent is built. + /// Typically that Widget will have been configured with further children, + /// such that really this function returns a tree of configuration. Widget build(); } -abstract class ComponentState { - ComponentStateElement _holder; +/// StatefulComponents provide the configuration for +/// [StatefulComponentElement]s, which wrap [ComponentState]s, which hold +/// mutable state and can dynamically and spontaneously ask to be rebuilt. +abstract class StatefulComponent extends Widget { + const StatefulComponent({ Key key }) : super(key: key); - void setState(void fn()) { - fn(); - _holder.scheduleBuild(); - } - - T get config => _config; - T _config; - - /// Override this setter to update additional state when the config changes. - void set config(T config) { - _config = config; - } - - /// Called when this object is removed from the tree - void didUnmount() { } - - Widget build(); -} - -abstract class ComponentConfiguration extends Widget { - ComponentConfiguration({ Key key }) : super(key); - - ComponentStateElement createElement() => new ComponentStateElement(this); + /// StatefulComponents always use StatefulComponentElements to represent + /// themselves in the Element tree. + StatefulComponentElement createElement() => new StatefulComponentElement(this); + /// Returns an instance of the state to which this StatefulComponent is + /// related, using this object as the configuration. Subclasses should + /// override this to return a new instance of the ComponentState class + /// associated with this StatefulComponent class, like this: + /// + /// MyComponentState createState() => new MyComponentState(this); ComponentState createState(); } -bool _canUpdate(Widget oldWidget, Widget newWidget) { - return oldWidget.runtimeType == newWidget.runtimeType - && oldWidget.key == newWidget.key; +/// The logic and internal state for a StatefulComponent. +abstract class ComponentState { + ComponentState(this._config); + + StatefulComponentElement _element; + + /// Whenever you need to change internal state for a ComponentState object, + /// make the change in a function that you pass to setState(), as in: + /// + /// setState(() { myState = newValue }); + /// + /// If you just change the state directly without calling setState(), then + /// the component will not be scheduled for rebuilding, meaning that its + /// rendering will not be updated. + void setState(void fn()) { + fn(); + _element.scheduleBuild(); + } + + /// The current configuration (an instance of the corresponding + /// StatefulComponent class). + T get config => _config; + T _config; + + /// Called whenever the configuration changes. Override this method to update + /// additional state when the config field's value is changed. + void didUpdateConfig(T oldConfig) { } + + /// Called when this object is removed from the tree. Override this to clean + /// up any resources allocated by this object. + void dispose() { } + + /// Returns another Widget out of which this StatefulComponent is built. + /// Typically that Widget will have been configured with further children, + /// such that really this function returns a tree of configuration. + Widget build(); } -void _debugReportException(String context, dynamic exception, StackTrace stack) { - print('------------------------------------------------------------------------'); - 'Exception caught while $context'.split('\n').forEach(print); - print('$exception'); - print('Stack trace:'); - '$stack'.split('\n').forEach(print); - print('------------------------------------------------------------------------'); +bool _canUpdate(Widget oldWidget, Widget newWidget) { + return oldWidget.runtimeType == newWidget.runtimeType && + oldWidget.key == newWidget.key; } enum _ElementLifecycle { @@ -108,15 +164,23 @@ abstract class Element { } Element _parent; + + /// information set by parent to define where this child fits in its parent's + /// child list dynamic _slot; + + /// an integer that is guaranteed to be greater than the parent's, if any int _depth; + /// the configuration for this element T _widget; _ElementLifecycle _lifecycleState = _ElementLifecycle.initial; + /// Calls the argument for each child. Must be overridden by subclasses that support having children. void visitChildren(ElementVisitor visitor) { } + /// Calls the argument for each descendant, depth-first pre-order. void visitDescendants(ElementVisitor visitor) { void walk(Element element) { visitor(element); @@ -146,13 +210,13 @@ abstract class Element { _slot = slot; } - void update(T updated) { - assert(updated != null); + void update(T newWidget) { + assert(newWidget != null); assert(_lifecycleState == _ElementLifecycle.mounted); assert(_widget != null); assert(_depth != null); - assert(_canUpdate(_widget, updated)); - _widget = updated; + assert(_canUpdate(_widget, newWidget)); + _widget = newWidget; } void unmount() { @@ -272,6 +336,8 @@ class _BuildScheduler { final _BuildScheduler _buildScheduler = new _BuildScheduler(); +typedef Widget WidgetBuilder(); + abstract class BuildableElement extends Element { BuildableElement(T widget) : super(widget); @@ -317,40 +383,42 @@ abstract class BuildableElement extends Element { } } -class ComponentElement extends BuildableElement { - ComponentElement(Component component) : super(component) { +class StatelessComponentElement extends BuildableElement { + StatelessComponentElement(StatelessComponent component) : super(component) { _builder = component.build; } - void update(Component updated) { - super.update(updated); - assert(_widget == updated); + void update(StatelessComponent newWidget) { + super.update(newWidget); + assert(_widget == newWidget); _builder = _widget.build; _rebuild(); } } -class ComponentStateElement extends BuildableElement { - ComponentStateElement(ComponentConfiguration configuration) +class StatefulComponentElement extends BuildableElement { + StatefulComponentElement(StatefulComponent configuration) : _state = configuration.createState(), super(configuration) { + assert(_state._config == configuration); + _state._element = this; _builder = _state.build; - _state._holder = this; - _state.config = configuration; } ComponentState get state => _state; ComponentState _state; - void update(ComponentConfiguration updated) { - super.update(updated); - assert(_widget == updated); - _state.config = _widget; + void update(StatefulComponent newWidget) { + super.update(newWidget); + assert(_widget == newWidget); + StatefulComponent oldConfig = _state._config; + _state._config = _widget; + _state.didUpdateConfig(oldConfig); _rebuild(); } void unmount() { super.unmount(); - _state.didUnmount(); + _state.dispose(); _state = null; } } @@ -441,3 +509,26 @@ class OneChildRenderObjectElement extends class MultiChildRenderObjectElement extends RenderObjectElement { MultiChildRenderObjectElement(T widget) : super(widget); } + + + +typedef void WidgetsExceptionHandler(String context, dynamic exception, StackTrace stack); +/// This callback is invoked whenever an exception is caught by the widget +/// system. The 'context' argument is a description of what was happening when +/// the exception occurred, and may include additional details such as +/// descriptions of the objects involved. The 'exception' argument contains the +/// object that was thrown, and the 'stack' argument contains the stack trace. +/// The callback is invoked after the information is printed to the console, and +/// could be used to print additional information, such as from +/// [debugDumpApp()]. +WidgetsExceptionHandler debugWidgetsExceptionHandler; +void _debugReportException(String context, dynamic exception, StackTrace stack) { + print('------------------------------------------------------------------------'); + 'Exception caught while $context'.split('\n').forEach(print); + print('$exception'); + print('Stack trace:'); + '$stack'.split('\n').forEach(print); + if (debugWidgetsExceptionHandler != null) + debugWidgetsExceptionHandler(context, exception, stack); + print('------------------------------------------------------------------------'); +} diff --git a/sky/unit/test/fn3/stateful_component_test.dart b/sky/unit/test/fn3/stateful_component_test.dart index 2a50624f2d4..204581cd789 100644 --- a/sky/unit/test/fn3/stateful_component_test.dart +++ b/sky/unit/test/fn3/stateful_component_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; import 'widget_tester.dart'; -class TestComponentConfig extends ComponentConfiguration { +class TestComponentConfig extends StatefulComponent { TestComponentConfig({ this.left, this.right }); final Widget left; @@ -13,9 +13,7 @@ class TestComponentConfig extends ComponentConfiguration { TestComponentState createState() => new TestComponentState(); } -class TestComponentState extends ComponentState { - TestComponentConfig get config => super.config; - +class TestComponentState extends ComponentState { bool _showLeft = true; void flip() { @@ -32,7 +30,7 @@ class TestComponentState extends ComponentState { final BoxDecoration kBoxDecorationA = new BoxDecoration(); final BoxDecoration kBoxDecorationB = new BoxDecoration(); -class TestBuildCounter extends Component { +class TestBuildCounter extends StatelessComponent { static int buildCount = 0; Widget build() { @@ -42,8 +40,8 @@ class TestBuildCounter extends Component { } void flipStatefulComponent(WidgetTester tester) { - ComponentStateElement stateElement = - tester.findElement((element) => element is ComponentStateElement); + StatefulComponentElement stateElement = + tester.findElement((element) => element is StatefulComponentElement); (stateElement.state as TestComponentState).flip(); } diff --git a/sky/unit/test/fn3/widget_tester.dart b/sky/unit/test/fn3/widget_tester.dart index 5eca02c43e6..1716f9936fb 100644 --- a/sky/unit/test/fn3/widget_tester.dart +++ b/sky/unit/test/fn3/widget_tester.dart @@ -1,6 +1,6 @@ import 'package:sky/src/fn3/framework.dart'; -class TestComponent extends Component { +class TestComponent extends StatelessComponent { TestComponent({ this.child }); final Widget child; Widget build() => child; @@ -9,7 +9,7 @@ class TestComponent extends Component { final Object _rootSlot = new Object(); class WidgetTester { - ComponentElement _rootElement; + StatelessComponentElement _rootElement; void walkElements(ElementVisitor visitor) { void walk(Element element) { @@ -36,7 +36,7 @@ class WidgetTester { void pumpFrame(Widget widget) { if (_rootElement == null) { - _rootElement = new ComponentElement(new TestComponent(child: widget)); + _rootElement = new StatelessComponentElement(new TestComponent(child: widget)); _rootElement.mount(_rootSlot); } else { _rootElement.update(new TestComponent(child: widget));