diff --git a/examples/fn/README.md b/examples/fn/README.md index 4bd61756b48..061a167fcf3 100644 --- a/examples/fn/README.md +++ b/examples/fn/README.md @@ -34,20 +34,20 @@ main() { import '../fn/lib/fn.dart'; class HelloWorldApp extends App { - Node render() { + Node build() { return new Text('Hello, World!'); } } ``` -An app is comprised of (and is, itself, a) components. A component's main job is to implement `Node render()`. The idea here is that the `render` method describes the DOM of a component at any given point during its lifetime. In this case, our `HelloWorldApp`'s `render` method just returns a `Text` node which displays the obligatory line of text. +An app is comprised of (and is, itself, a) components. A component's main job is to implement `Node build()`. The idea here is that the `build` method describes the DOM of a component at any given point during its lifetime. In this case, our `HelloWorldApp`'s `build` method just returns a `Text` node which displays the obligatory line of text. Nodes ----- -A component's `render` method must return a single `Node` which *may* have children (and so on, forming a *subtree*). Effen comes with a few built-in nodes which mirror the built-in nodes/elements of sky: `Text`, `Anchor` (``, `Image` (``) and `Container` (`
`). `render` can return a tree of Nodes comprised of any of these nodes and plus any other imported object which extends `Component`. +A component's `build` method must return a single `Node` which *may* have children (and so on, forming a *subtree*). Effen comes with a few built-in nodes which mirror the built-in nodes/elements of sky: `Text`, `Anchor` (``, `Image` (``) and `Container` (`
`). `build` can return a tree of Nodes comprised of any of these nodes and plus any other imported object which extends `Component`. How to structure you app ------------------------ -If you're familiar with React, the basic idea is the same: Application data flows *down* from components which have data to components & nodes which they construct via construction parameters. Generally speaking, View-Model data (data which is derived from *model* data, but exists only because the view needs it), is computed during the course of `render` and is short-lived, being handed into nodes & components as configuration data. +If you're familiar with React, the basic idea is the same: Application data flows *down* from components which have data to components & nodes which they construct via construction parameters. Generally speaking, View-Model data (data which is derived from *model* data, but exists only because the view needs it), is computed during the course of `build` and is short-lived, being handed into nodes & components as configuration data. What does "data flowing down the tree" mean? -------------------------------------------- @@ -68,13 +68,13 @@ All components have access to two kinds of state: (1) data which is handing in f All nodes and most components should be stateless, never needing to mutate themselves and only reacting to data which is handed into them. Some components will be stateful. This state will likely encapsulate transient states of the UI, such as scroll position, animation state, uncommitted form values, etc... -A component can become stateful in two ways: (1) by passing `super(stateful: true)` to its call to the superclasses constructor, or by calling `setState(Function fn)`. The former is a way to have a component start its life stateful, and the later results in the component becoming statefull *as well as* scheduling the component to re-render at the end of the current animation frame. +A component can become stateful in two ways: (1) by passing `super(stateful: true)` to its call to the superclasses constructor, or by calling `setState(Function fn)`. The former is a way to have a component start its life stateful, and the later results in the component becoming statefull *as well as* scheduling the component to re-build at the end of the current animation frame. -What does it mean to be stateful? It means that the diffing mechanism retains the specific *instance* of the component as long as the component which renders it continues to require its presence. The component which constructed it may have provided new configuration in form of different values for the constructor parameters, but these values (public fields) will be copied (using reflection) onto the retained instance whose privates fields are left unmodified. +What does it mean to be stateful? It means that the diffing mechanism retains the specific *instance* of the component as long as the component which builds it continues to require its presence. The component which constructed it may have provided new configuration in form of different values for the constructor parameters, but these values (public fields) will be copied (using reflection) onto the retained instance whose privates fields are left unmodified. Rendering --------- -At the end of each animation frame, all components (including the root `App`) which have `setState` on themselves will be re-rendered and the resulting changes will be minimally applied to the DOM. Note that components of lower "order" (those near the root of the tree) will render first because their rendering may require re-rendering of higher order (those near the leaves), thus avoiding the possibility that a component which is dirty render more than once during a single cycle. +At the end of each animation frame, all components (including the root `App`) which have `setState` on themselves will be rebuilt and the resulting changes will be minimally applied to the DOM. Note that components of lower "order" (those near the root of the tree) will build first because their building may require rebuilding of higher order (those near the leaves), thus avoiding the possibility that a component which is dirty build more than once during a single cycle. Keys ---- @@ -90,8 +90,8 @@ class MyComp extends Component { Object key, sky.EventListener onClick // delegated handler }) : super(key: key); - - Node render() { + + Node build() { return new Container( onClick: onClick, onScrollStart: _handleScroll // direct handler @@ -116,20 +116,20 @@ Styling is the part of Effen which is least designed and is likely to change. At Animation --------- -Animation is still an area of exploration. The pattern which is presently used in the `stocks-fn` example is the following: Components which are animatable should contain within their implementation file an Animation object whose job it is to react to events and control an animation by exposing one or more Dart `stream`s of data. The `Animation` object is owned by the owner (or someone even higher) and the stream is passed into the animating component via its constructor. The first time the component renders, it listens on the stream and calls `setState` on itself for each value which emerges from the stream [See the `drawer.dart` widget for an example]. +Animation is still an area of exploration. The pattern which is presently used in the `stocks-fn` example is the following: Components which are animatable should contain within their implementation file an Animation object whose job it is to react to events and control an animation by exposing one or more Dart `stream`s of data. The `Animation` object is owned by the owner (or someone even higher) and the stream is passed into the animating component via its constructor. The first time the component builds, it listens on the stream and calls `setState` on itself for each value which emerges from the stream [See the `drawer.dart` widget for an example]. Performance ----------- Isn't diffing the DOM expensive? This is kind of a subject question with a few answers, but the biggest issue is what do you mean by "fast"? -The stock answer is that diffing the DOM is fast because you compute the diff of the current VDOM from the previous VDOM and only apply the diffs to the actual DOM. The truth that this is fast, but not really fast enough to re-render everything on the screen for 60 or 120fps animations on a mobile device. +The stock answer is that diffing the DOM is fast because you compute the diff of the current VDOM from the previous VDOM and only apply the diffs to the actual DOM. The truth that this is fast, but not really fast enough to rebuild everything on the screen for 60 or 120fps animations on a mobile device. -The answer that many people don't get is that there are really two logical types of renders: (1) When underlying model data changes: This generally requires handing in new data to the root component (in Effen, this means the `App` calling `setState` on itself). (2) When user interaction updates a control or an animation takes place. (1) is generally more expensive because it requires a full rendering & diff, but tends to happen infrequently. (2) tends to happen frequently, but at nodes which are near the leafs of the tree, so the number of nodes which must be reconsiled is generally small. +The answer that many people don't get is that there are really two logical types of builds: (1) When underlying model data changes: This generally requires handing in new data to the root component (in Effen, this means the `App` calling `setState` on itself). (2) When user interaction updates a control or an animation takes place. (1) is generally more expensive because it requires a full building & diff, but tends to happen infrequently. (2) tends to happen frequently, but at nodes which are near the leafs of the tree, so the number of nodes which must be reconsiled is generally small. -React provides a way to manually insist that a componet not re-render based on its old and new state (and they encourage the use of immutable data structures because discovering the data is the same can be accomplished with a reference comparison). A similar mechanism is in the works for Effen. +React provides a way to manually insist that a componet not rebuild based on its old and new state (and they encourage the use of immutable data structures because discovering the data is the same can be accomplished with a reference comparison). A similar mechanism is in the works for Effen. -Lastly, Effen does something unique: Because its diffing is component-wise, it can be smart about not forcing the re-render of components which are handed in as *arguments* when only the component itself is dirty. For example, the `drawer.dart` component knows how to animate out & back and expose a content pane -- but it takes its content pane as an argument. When the animation mutates the inlineStyle of the `Drawer`'s `Container`, it must schedule itself for re-render -- but -- because the content was handed in to its constructor, its configuration can't have changed and Effen doesn't require it to re-render. +Lastly, Effen does something unique: Because its diffing is component-wise, it can be smart about not forcing the rebuild of components which are handed in as *arguments* when only the component itself is dirty. For example, the `drawer.dart` component knows how to animate out & back and expose a content pane -- but it takes its content pane as an argument. When the animation mutates the inlineStyle of the `Drawer`'s `Container`, it must schedule itself for rebuild -- but -- because the content was handed in to its constructor, its configuration can't have changed and Effen doesn't require it to rebuild. -It is a design goal that it should be *possible* to arrange that all "render" cycles which happen during animations can complete in less than one milliesecond on a Nexus 5. +It is a design goal that it should be *possible* to arrange that all "build" cycles which happen during animations can complete in less than one milliesecond on a Nexus 5. diff --git a/examples/fn/widgets/box.dart b/examples/fn/widgets/box.dart index cfddab1a3bf..7a1bffd52b6 100644 --- a/examples/fn/widgets/box.dart +++ b/examples/fn/widgets/box.dart @@ -27,7 +27,7 @@ class Box extends Component { Box({String key, this.title, this.children }) : super(key: key); - Node render() { + Node build() { return new Container( style: _style, children: [ diff --git a/examples/fn/widgets/button.dart b/examples/fn/widgets/button.dart index c7f53cac5ba..17ffa2524bc 100644 --- a/examples/fn/widgets/button.dart +++ b/examples/fn/widgets/button.dart @@ -29,11 +29,11 @@ class Button extends ButtonBase { Button({ Object key, this.content }) : super(key: key); - Node render() { + Node build() { return new Container( key: 'Button', style: _highlight ? _highlightStyle : _style, - children: [super.render(), content] + children: [super.build(), content] ); } } diff --git a/examples/fn/widgets/checkbox.dart b/examples/fn/widgets/checkbox.dart index 2825498d61a..aa76471e014 100644 --- a/examples/fn/widgets/checkbox.dart +++ b/examples/fn/widgets/checkbox.dart @@ -54,11 +54,11 @@ class Checkbox extends ButtonBase { Checkbox({ Object key, this.onChanged, this.checked }) : super(key: key); - Node render() { + Node build() { return new Container( style: _style, children: [ - super.render(), + super.build(), new Container( style: _highlight ? _containerHighlightStyle : _containerStyle, children: [ diff --git a/examples/fn/widgets/drawer.dart b/examples/fn/widgets/drawer.dart index f4a3aa00bbf..8e210d8eb70 100644 --- a/examples/fn/widgets/drawer.dart +++ b/examples/fn/widgets/drawer.dart @@ -152,7 +152,7 @@ class Drawer extends Component { }); } - Node render() { + Node build() { _ensureListening(); bool isClosed = _position <= -_kWidth; diff --git a/examples/fn/widgets/drawerheader.dart b/examples/fn/widgets/drawerheader.dart index b72e302d223..052dad5120a 100644 --- a/examples/fn/widgets/drawerheader.dart +++ b/examples/fn/widgets/drawerheader.dart @@ -27,7 +27,7 @@ class DrawerHeader extends Component { DrawerHeader({ Object key, this.children }) : super(key: key); - Node render() { + Node build() { return new Container( style: _style, children: [ diff --git a/examples/fn/widgets/fixedheightscrollable.dart b/examples/fn/widgets/fixedheightscrollable.dart index ea08322f93a..4fee244c178 100644 --- a/examples/fn/widgets/fixedheightscrollable.dart +++ b/examples/fn/widgets/fixedheightscrollable.dart @@ -32,7 +32,7 @@ abstract class FixedHeightScrollable extends Component { this.maxOffset }) : super(key: key) {} - List renderItems(int start, int count); + List buildItems(int start, int count); void didMount() { var root = getRoot(); @@ -48,7 +48,7 @@ abstract class FixedHeightScrollable extends Component { }); } - Node render() { + Node build() { var itemNumber = 0; var drawCount = 1; var transformStyle = ''; @@ -73,7 +73,7 @@ abstract class FixedHeightScrollable extends Component { new Container( style: _scrollAreaStyle, inlineStyle: transformStyle, - children: renderItems(itemNumber, drawCount) + children: buildItems(itemNumber, drawCount) ) ] ) diff --git a/examples/fn/widgets/floating_action_button.dart b/examples/fn/widgets/floating_action_button.dart index ca70e5dc0d4..ceebc01c270 100644 --- a/examples/fn/widgets/floating_action_button.dart +++ b/examples/fn/widgets/floating_action_button.dart @@ -30,8 +30,8 @@ class FloatingActionButton extends MaterialComponent { FloatingActionButton({ Object key, this.content }) : super(key: key); - Node render() { - List children = [super.render()]; + Node build() { + List children = [super.build()]; if (content != null) children.add(content); diff --git a/examples/fn/widgets/icon.dart b/examples/fn/widgets/icon.dart index 8f76d8f4104..263d3f1711d 100644 --- a/examples/fn/widgets/icon.dart +++ b/examples/fn/widgets/icon.dart @@ -15,7 +15,7 @@ class Icon extends Component { this.type: '' }) : super(key: key); - Node render() { + Node build() { String category = ''; String subtype = ''; List parts = type.split('/'); diff --git a/examples/fn/widgets/inksplash.dart b/examples/fn/widgets/inksplash.dart index c5abb270942..cfc359af160 100644 --- a/examples/fn/widgets/inksplash.dart +++ b/examples/fn/widgets/inksplash.dart @@ -80,7 +80,7 @@ class InkSplash extends Component { }); } - Node render() { + Node build() { _ensureListening(); return new Container( diff --git a/examples/fn/widgets/material.dart b/examples/fn/widgets/material.dart index 56aa417ac31..b0a828295d6 100644 --- a/examples/fn/widgets/material.dart +++ b/examples/fn/widgets/material.dart @@ -17,7 +17,7 @@ abstract class MaterialComponent extends Component { MaterialComponent({ Object key }) : super(key: key); - Node render() { + Node build() { List children = []; if (_splashes != null) { diff --git a/examples/fn/widgets/menudivider.dart b/examples/fn/widgets/menudivider.dart index b6de4a19693..25b83505c14 100644 --- a/examples/fn/widgets/menudivider.dart +++ b/examples/fn/widgets/menudivider.dart @@ -9,7 +9,7 @@ class MenuDivider extends Component { MenuDivider({ Object key }) : super(key: key); - Node render() { + Node build() { return new Container( style: _style ); diff --git a/examples/fn/widgets/menuitem.dart b/examples/fn/widgets/menuitem.dart index 6a7407b1d5d..85f8d506671 100644 --- a/examples/fn/widgets/menuitem.dart +++ b/examples/fn/widgets/menuitem.dart @@ -35,11 +35,11 @@ class MenuItem extends ButtonBase { MenuItem({ Object key, this.icon, this.children }) : super(key: key); - Node render() { + Node build() { return new Container( style: _highlight ? _highlightStyle : _style, children: [ - super.render(), + super.build(), new Icon( style: _iconStyle, size: 24, diff --git a/examples/fn/widgets/radio.dart b/examples/fn/widgets/radio.dart index c5c1b41f806..240e5a2c19a 100644 --- a/examples/fn/widgets/radio.dart +++ b/examples/fn/widgets/radio.dart @@ -45,11 +45,11 @@ class Radio extends ButtonBase { this.groupValue }) : super(key: key); - Node render() { + Node build() { return new Container( style: _highlight ? _highlightStyle : _style, children: value == groupValue ? - [super.render(), new Container( style : _dotStyle )] : [super.render()] + [super.build(), new Container( style : _dotStyle )] : [super.build()] )..events.listen('click', _handleClick); } diff --git a/examples/fn/widgets/toolbar.dart b/examples/fn/widgets/toolbar.dart index a4afc5c1b6e..c5ccb0dea68 100644 --- a/examples/fn/widgets/toolbar.dart +++ b/examples/fn/widgets/toolbar.dart @@ -16,7 +16,7 @@ class Toolbar extends Component { Toolbar({String key, this.children}) : super(key: key); - Node render() { + Node build() { return new Container( style: _style, children: children diff --git a/examples/stocks-fn/stockarrow.dart b/examples/stocks-fn/stockarrow.dart index 8285e628010..82e52815d2e 100644 --- a/examples/stocks-fn/stockarrow.dart +++ b/examples/stocks-fn/stockarrow.dart @@ -67,7 +67,7 @@ class StockArrow extends Component { return _kRedColors[_colorIndexForPercentChange(percentChange)]; } - Node render() { + Node build() { String border = _colorForPercentChange(percentChange).toString(); bool up = percentChange > 0; String type = up ? 'bottom' : 'top'; diff --git a/examples/stocks-fn/stocklist.dart b/examples/stocks-fn/stocklist.dart index c4ac95fd4b2..9f0de09196a 100644 --- a/examples/stocks-fn/stocklist.dart +++ b/examples/stocks-fn/stocklist.dart @@ -9,7 +9,7 @@ class Stocklist extends FixedHeightScrollable { this.stocks }) : super(key: key, minOffset: 0.0); - List renderItems(int start, int count) { + List buildItems(int start, int count) { var items = []; for (var i = 0; i < count; i++) { items.add(new StockRow(stock: stocks[start + i])); diff --git a/examples/stocks-fn/stockrow.dart b/examples/stocks-fn/stockrow.dart index 89354cb558c..5ee0313c54c 100644 --- a/examples/stocks-fn/stockrow.dart +++ b/examples/stocks-fn/stockrow.dart @@ -34,7 +34,7 @@ class StockRow extends MaterialComponent { this.stock = stock; } - Node render() { + Node build() { String lastSale = "\$${stock.lastSale.toStringAsFixed(2)}"; String changeInPrice = "${stock.percentChange.toStringAsFixed(2)}%"; @@ -62,7 +62,7 @@ class StockRow extends MaterialComponent { ) ]; - children.add(super.render()); + children.add(super.build()); return new Container( style: _style, diff --git a/examples/stocks-fn/stocksapp.dart b/examples/stocks-fn/stocksapp.dart index 154c17c4e44..acba86f07af 100644 --- a/examples/stocks-fn/stocksapp.dart +++ b/examples/stocks-fn/stocksapp.dart @@ -33,7 +33,7 @@ class StocksApp extends App { StocksApp() : super(); - Node render() { + Node build() { var drawer = new Drawer( animation: _drawerAnimation, children: [ diff --git a/framework/fn.dart b/framework/fn.dart index 380dc52ebf9..06853293f72 100644 --- a/framework/fn.dart +++ b/framework/fn.dart @@ -244,7 +244,7 @@ abstract class Element extends Node { Element oldElement = old as Element; if (oldElement == null) { - // print("...no oldElement, initial render"); + // print("...no oldElement, initial build"); _root = sky.document.createElement(_tagName); _parentInsertBefore(host, _root, insertBefore); @@ -502,21 +502,21 @@ class Anchor extends Element { } List _dirtyComponents = new List(); -bool _renderScheduled = false; +bool _buildScheduled = false; bool _inRenderDirtyComponents = false; -void _renderDirtyComponents() { +void _buildDirtyComponents() { try { _inRenderDirtyComponents = true; Stopwatch sw = new Stopwatch()..start(); _dirtyComponents.sort((a, b) => a._order - b._order); for (var comp in _dirtyComponents) { - comp._renderIfDirty(); + comp._buildIfDirty(); } _dirtyComponents.clear(); - _renderScheduled = false; + _buildScheduled = false; sw.stop(); if (_shouldLogRenderDuration) @@ -530,15 +530,15 @@ void _scheduleComponentForRender(Component c) { assert(!_inRenderDirtyComponents); _dirtyComponents.add(c); - if (!_renderScheduled) { - _renderScheduled = true; - new Future.microtask(_renderDirtyComponents); + if (!_buildScheduled) { + _buildScheduled = true; + new Future.microtask(_buildDirtyComponents); } } abstract class Component extends Node { - bool _dirty = true; // components begin dirty because they haven't rendered. - Node _rendered = null; + bool _dirty = true; // components begin dirty because they haven't built. + Node _vdom = null; bool _removed = false; final int _order; static int _currentOrder = 0; @@ -554,10 +554,10 @@ abstract class Component extends Node { void didUnmount() {} void _remove() { - assert(_rendered != null); + assert(_vdom != null); assert(_root != null); - _rendered._remove(); - _rendered = null; + _vdom._remove(); + _vdom = null; _root = null; _removed = true; didUnmount(); @@ -571,89 +571,89 @@ abstract class Component extends Node { Component oldComponent = old as Component; if (oldComponent == null || oldComponent == this) { - _renderInternal(host, insertBefore); + _buildInternal(host, insertBefore); return false; } assert(oldComponent != null); assert(_dirty); - assert(_rendered == null); + assert(_vdom == null); if (oldComponent._stateful) { - _stateful = false; // prevent iloop from _renderInternal below. + _stateful = false; // prevent iloop from _buildInternal below. reflect.copyPublicFields(this, oldComponent); oldComponent._dirty = true; _dirty = false; - oldComponent._renderInternal(host, insertBefore); + oldComponent._buildInternal(host, insertBefore); return true; // Must retain old component } - _rendered = oldComponent._rendered; - _renderInternal(host, insertBefore); + _vdom = oldComponent._vdom; + _buildInternal(host, insertBefore); return false; } - void _renderInternal(sky.Node host, sky.Node insertBefore) { + void _buildInternal(sky.Node host, sky.Node insertBefore) { if (!_dirty) { - assert(_rendered != null); + assert(_vdom != null); return; } - var oldRendered = _rendered; + var oldRendered = _vdom; bool mounting = oldRendered == null; int lastOrder = _currentOrder; _currentOrder = _order; _currentlyRendering = this; - _rendered = render(); + _vdom = build(); _currentlyRendering = null; _currentOrder = lastOrder; - _rendered.events.addAll(events); + _vdom.events.addAll(events); _dirty = false; // TODO(rafaelw): This eagerly removes the old DOM. It may be that a - // new component was rendered that could re-use some of it. Consider + // new component was built that could re-use some of it. Consider // syncing the new VDOM against the old one. if (oldRendered != null && - _rendered.runtimeType != oldRendered.runtimeType) { + _vdom.runtimeType != oldRendered.runtimeType) { oldRendered._remove(); oldRendered = null; } - if (_rendered._sync(oldRendered, host, insertBefore)) { - _rendered = oldRendered; // retain stateful component + if (_vdom._sync(oldRendered, host, insertBefore)) { + _vdom = oldRendered; // retain stateful component } - _root = _rendered._root; - assert(_rendered._root is sky.Node); + _root = _vdom._root; + assert(_vdom._root is sky.Node); if (mounting) { didMount(); } } - void _renderIfDirty() { + void _buildIfDirty() { if (_removed) return; - assert(_rendered != null); + assert(_vdom != null); - var rendered = _rendered; - while (rendered is Component) { - rendered = rendered._rendered; + var vdom = _vdom; + while (vdom is Component) { + vdom = vdom._vdom; } - assert(rendered._root != null); - sky.Node root = rendered._root; + assert(vdom._root != null); + sky.Node root = vdom._root; - _renderInternal(root.parentNode, root.nextSibling); + _buildInternal(root.parentNode, root.nextSibling); } void setState(Function fn()) { - assert(_rendered != null || _removed); // cannot setState before mounting. + assert(_vdom != null || _removed); // cannot setState before mounting. _stateful = true; fn(); if (!_removed && _currentlyRendering != this) { @@ -662,7 +662,7 @@ abstract class Component extends Node { } } - Node render(); + Node build(); } abstract class App extends Component { @@ -679,7 +679,7 @@ abstract class App extends Component { sw.stop(); if (_shouldLogRenderDuration) - print("Initial render: ${sw.elapsedMicroseconds} microseconds"); + print("Initial build: ${sw.elapsedMicroseconds} microseconds"); }); } }