Commit b136deda authored by Adam Barth's avatar Adam Barth

Move Getting Started and Tutorial to GitHub pages

Along with some other docs.
parent 0f4d3765
Getting Started with Sky
========================
Sky
===
Sky apps are written in Dart. To get started, we need to set up Dart SDK:
Sky is a new way to build high-performance, cross-platform mobile apps.
Sky is optimized for today's, and tomorrow's, mobile devices. We are
focused on low-latency input and high frame rates on Android and iOS.
- Install the [Dart SDK](https://www.dartlang.org/downloads/):
- Mac: `brew tap dart-lang/dart && brew install dart --devel`
- Linux: See [https://www.dartlang.org/downloads/linux.html](https://www.dartlang.org/downloads/linux.html)
- Ensure that `$DART_SDK` is set to the path of your Dart SDK and that the
`dart` and `pub` executables are on your `$PATH`.
Once you have installed Dart SDK, create a new directory and add a
[pubspec.yaml](https://www.dartlang.org/tools/pub/pubspec.html):
```yaml
name: your_app_name
dependencies:
sky: any
sky_tools: any
```
Next, create a `lib` directory (which is where your Dart code will go) and use
the `pub` tool to fetch the Sky package and its dependencies:
- `mkdir lib`
- `pub upgrade`
Sky assumes the entry point for your application is a `main` function in
`lib/main.dart`:
```dart
import 'package:sky/widgets.dart';
class HelloWorldApp extends App {
Widget build() {
return new Center(child: new Text('Hello, world!'));
}
}
void main() {
runApp(new HelloWorldApp());
}
```
Execution starts in `main`, which in this example runs a new instance of the `HelloWorldApp`.
The `HelloWorldApp` builds a `Text` widget containing the traditional `Hello, world!`
string and centers it on the screen using a `Center` widget. To learn more about
the widget system, please see the
[widgets tutorial](https://github.com/domokit/sky_engine/blob/master/sky/packages/sky/lib/src/widgets/README.md).
Setting up your Android device
-------------------------
Currently Sky requires an Android device running the Lollipop (or newer) version
of the Android operating system.
- Install the `adb` tool from the [Android SDK](https://developer.android.com/sdk/installing/index.html?pkg=tools):
- Mac: `brew install android-platform-tools`
- Linux: `sudo apt-get install android-tools-adb`
- If the version of `adb` provided by your Linux distribution is too old,
you might need to [install the Android SDK manually](https://developer.android.com/sdk/installing/index.html?pkg=tools]).
- Enable developer mode on your device by visiting `Settings > About phone`
and tapping the `Build number` field five times.
- Enable `Android debugging` in `Settings > Developer options`.
- Using a USB cable, plug your phone into your computer. If prompted on your
device, authorize your computer to access your device.
Running a Sky application
-------------------------
The `sky` pub package includes a `sky_tool` script to assist in running
Sky applications inside the `SkyShell.apk` harness. The `sky_tool` script
expects to be run from the root directory of your application's package (i.e.,
the same directory that contains the `pubspec.yaml` file).
To run your app with logging, run this command:
- `./packages/sky/sky_tool start --checked && ./packages/sky/sky_tool logs`
The `sky_tool start` command starts the dev server and uploads your app to the device, installing `SkyShell.apk` if needed.
The `--checked` flag triggers checked mode, in which types are checked, asserts are run, and
various [debugging features](https://github.com/domokit/sky_engine/blob/master/sky/packages/sky/lib/base/debug.dart) are enabled.
The `sky_tool logs` command logs errors and Dart `print()` output from the app, automatically limiting the output to just output from Sky Dart code and the Sky Engine C++ code (which
for historical reasons currently uses the tag `chromium`.)
To avoid confusion from old log messages, you may wish to call `sky_tool logs --clear` before calling
`sky_tool start`, to clear the log between runs.
Rapid Iteration
---------------
As an alternative to running `./packages/sky/sky_tool start` every time you make a change,
you might prefer to have the SkyShell reload your app automatically for you as you edit. To
do this, run the following command:
- `./packages/sky/sky_tool listen`
This is a long-running command -- just press `ctrl-c` when you want to stop listening for
changes to the file system and automatically reloading your app.
Currently `sky_tool listen` only works for Android, but iOS device and iOS simulator support
are coming soon.
Debugging
---------
Sky uses [Observatory](https://www.dartlang.org/tools/observatory/) for
debugging and profiling. While running your Sky app using `sky_tool`, you can
access Observatory by navigating your web browser to
[http://localhost:8181/](http://localhost:8181/).
Building a standalone APK
-------------------------
Although it is possible to build a standalone APK containing your application,
doing so right now is difficult. If you're feeling brave, you can see how we
build the `Stocks.apk` in
[examples/stocks](https://github.com/domokit/sky_engine/tree/master/examples/stocks).
Eventually we plan to make this much easier and support platforms other than
Android, but that work still in progress.
See the [getting started guide](https://flutter.github.io/getting-started/) for
information about using Sky.
Sky Rendering
=============
The Sky render tree is a low-level layout and painting system based on a
retained tree of objects that inherit from [`RenderObject`](object.dart). Most
developers using Sky will not need to interact directly with the rendering tree.
Instead, most developers should use [Sky widgets](../widgets/README.md), which
are built using the render tree.
Overview
--------
### Base Model
The base class for every node in the render tree is
[`RenderObject`](object.dart), which defines the base layout model. The base
layout mode is extremely general and can accomodate a large number of more
concrete layout models that can co-exist in the same tree. For example, the base
model does not commit to a fixed number of dimensions or even a cartesian
coordinate system. In this way, a single render tree can contain render objects
operating in three-dimensional space together with other render objects
operating in two-dimensional space, e.g., on the face of a cube in the three-
dimensional space. Moreover, the two-dimensional layout might be partially
computed in cartesian coordinates and partially computed in polar coordinates.
These distinct models can interact during layout, for example determining the
size of the cube by the height of a block of text on the cube's face.
Not entirely free-wheeling, the base model does impose some structure on the
render tree:
* Subclasses of `RenderObject` must implement a `performLayout` function that
takes as input a `constraints` object provided by its parent. `RenderObject`
has no opinion about the structure of this object and different layout models
use different types of constraints. However, whatever type they choose must
implement `operator==` in such a way that `performLayout` produces the same
output for two `constraints` objects that are `operator==`.
* Implementations of `performLayout` are expected to call `layout` on their
children. When calling `layout`, a `RenderObject` must use the
`parentUsesSize` parameter to declare whether its `performLayout` function
depends on information read from the child. If the parent doesn't declare
that it uses the child's size, the edge from the parent to the child becomes
a _relayout boundary_, which means the child (and its subtree) might undergo
layout without the parent undergoing layout.
* Subclasses of `RenderObject` must implement a `paint` function that draws a
visual representation of the object onto a `PaintingCanvas`. If
the `RenderObject` has children, the `RenderObject` is responsible for
painting its children using the `paintChild` function on the
`PaintingCanvas`.
* Subclasses of `RenderObject` must call `adoptChild` whenever they add a
child. Similarly, they must call `dropChild` whenever they remove a child.
* Most subclasses of `RenderObject` will implement a `hitTest` function that
lets clients query the render tree for objects that intersect with a given
user input location. `RenderObject` itself does not impose a particular
type signature on `hitTest`, but most implementations will take an argument
of type `HitTestResult` (or, more likely, a model-specific subclass of
`HitTestResult`) as well as an object that describes the location at which
the user provided input (e.g., a `Point` for a two-dimensional cartesian
model).
* Finally, subclasses of `RenderObject` can override the default, do-nothing
implemenations of `handleEvent` and `rotate` to respond to user input and
screen rotation, respectively.
The base model also provides two mixins for common child models:
* `RenderObjectWithChildMixin` is useful for subclasses of `RenderObject` that
have a unique child.
* `ContainerRenderObjectMixin` is useful for subclasses of `RenderObject` that
have a child list.
Subclasses of `RenderObject` are not required to use either of these child
models and are free to invent novel child models for their specific use cases.
### Parent Data
TODO(ianh): Describe the parent data concept.
The `setupParentData()` method is automatically called for each child
when the child's parent is changed. However, if you need to
preinitialise the `parentData` member to set its values before you add
a node to its parent, you can preemptively call that future parent's
`setupParentData()` method with the future child as the argument.
TODO(ianh): Discuss putting per-child configuration information for
the parent on the child's parentData.
If you change a child's parentData dynamically, you must also call
markNeedsLayout() on the parent, otherwise the new information will
not take effect until something else triggers a layout.
### Box Model
#### Dimensions
All dimensions are expressed as logical pixel units. Font sizes are
also in logical pixel units. Logical pixel units are approximately
96dpi, but the precise value varies based on the hardware, in such a
way as to optimise for performance and rendering quality while keeping
interfaces roughly the same size across devices regardless of the
hardware pixel density.
Logical pixel units are automatically converted to device (hardware)
pixels when painting by applying an appropriate scale factor.
TODO(ianh): Define how you actually get the device pixel ratio if you
need it, and document best practices around that.
#### EdgeDims
#### BoxConstraints
### Bespoke Models
Using the provided subclasses
-----------------------------
### render_box.dart
#### RenderConstrainedBox
#### RenderShrinkWrapWidth
#### RenderOpacity
#### RenderColorFilter
#### RenderClipRect
#### RenderClipOval
#### RenderPadding
#### RenderPositionedBox
#### RenderImage
#### RenderDecoratedBox
#### RenderTransform
#### RenderSizeObserver
#### RenderCustomPaint
### RenderBlock (render_block.dart)
### RenderFlex (render_flex.dart)
### RenderParagraph (render_paragraph.dart)
### RenderStack (render_stack.dart)
Writing new subclasses
----------------------
### The RenderObject contract
If you want to define a `RenderObject` that uses a new coordinate
system, then you should inherit straight from `RenderObject`. Examples
of doing this can be found in [`RenderBox`](box.dart), which deals in
rectangles in cartesian space, and in the [sector_layout.dart
example](../../example/rendering/sector_layout.dart), which
implements a toy model based on polar coordinates. The `RenderView`
class, which is used internally to adapt from the host system to this
rendering framework, is another example.
A subclass of `RenderObject` must fulfill the following contract:
* It must fulfill the [AbstractNode contract](../base/README.md) when
dealing with children. Using `RenderObjectWithChildMixin` or
`ContainerRenderObjectMixin` can make this easier.
* Information about the child managed by the parent, e.g. typically
position information and configuration for the parent's layout,
should be stored on the `parentData` member; to this effect, a
ParentData subclass should be defined and the `setupParentData()`
method should be overriden to initialise the child's parent data
appropriately.
* Layout constraints must be expressed in a Constraints subclass. This
subclass must implement `operator==` (and `hashCode`).
* Whenever the layout needs updating, the `markNeedsLayout()` method
should be called.
* Whenever the rendering needs updating without changing the layout,
the `markNeedsPaint()` method should be called. (Calling
`markNeedsLayout()` implies a call to `markNeedsPaint()`, so you
don't need to call both.)
* The subclass must override `performLayout()` to perform layout based
on the constraints given in the `constraints` member. Each object is
responsible for sizing itself; positioning must be done by the
object calling `performLayout()`. Whether positioning is done before
or after the child's layout is a decision to be made by the class.
TODO(ianh): Document sizedByParent, performResize(), rotate
* TODO(ianh): Document painting, hit testing, debug*
#### The ParentData contract
#### Using RenderObjectWithChildMixin
#### Using ContainerRenderObjectMixin (and ContainerParentDataMixin)
This mixin can be used for classes that have a child list, to manage
the list. It implements the list using linked list pointers in the
`parentData` structure.
TODO(ianh): Document this mixin.
Subclasses must follow the following contract, in addition to the
contracts of any other classes they subclass:
* If the constructor takes a list of children, it must call addAll()
with that list.
TODO(ianh): Document how to walk the children.
### The RenderBox contract
A `RenderBox` subclass is required to implement the following contract:
* It must fulfill the [AbstractNode contract](../base/README.md) when
dealing with children. Note that using `RenderObjectWithChildMixin`
or `ContainerRenderObjectMixin` takes care of this for you, assuming
you fulfill their contract instead.
* If it has any data to store on its children, it must define a
BoxParentData subclass and override setupParentData() to initialise
the child's parent data appropriately, as in the following example.
(If the subclass has an opinion about what type its children must
be, e.g. the way that `RenderBlock` wants its children to be
`RenderBox` nodes, then change the `setupParentData()` signature
accordingly, to catch misuse of the method.)
```dart
class FooParentData extends BoxParentData { ... }
// In RenderFoo
void setupParentData(RenderObject child) {
if (child.parentData is! FooParentData)
child.parentData = new FooParentData();
}
```
* The class must encapsulate a layout algorithm that has the following
features:
** It uses as input a set of constraints, described by a
BoxConstraints object, and a set of zero or more children, as
determined by the class itself, and has as output a Size (which is
set on the object's own `size` field), and positions for each child
(which are set on the children's `parentData.position` field).
** The algorithm can decide the Size in one of two ways: either
exclusively based on the given constraints (i.e. it is effectively
sized entirely by its parent), or based on those constraints and
the dimensions of the children.
In the former case, the class must have a sizedByParent getter that
returns true, and it must have a `performResize()` method that uses
the object's `constraints` member to size itself by setting the
`size` member. The size must be consistent, a given set of
constraints must always result in the same size.
In the latter case, it will inherit the default `sizedByParent`
getter that returns false, and it will size itself in the
`performLayout()` function described below.
The `sizedByParent` distinction is purely a performance
optimisation. It allows nodes that only set their size based on the
incoming constraints to skip that logic when they need to be
re-laid-out, and, more importantly, it allows the layout system to
treat the node as a _layout boundary_, which reduces the amount of
work that needs to happen when the node is marked as needing
layout.
* The following methods must report numbers consistent with the output
of the layout algorithm used:
** `double getMinIntrinsicWidth(BoxConstraints constraints)` must
return the width that fits within the given constraints below which
making the width constraint smaller would not increase the
resulting height, or, to put it another way, the narrowest width at
which the box can be rendered without failing to lay the children
out within itself.
For example, the minimum intrinsic width of a piece of text like "a
b cd e", where the text is allowed to wrap at spaces, would be the
width of "cd".
** `double getMaxIntrinsicWidth(BoxConstraints constraints)` must
return the width that fits within the given constraints above which
making the width constraint larger would not decrease the resulting
height.
For example, the maximum intrinsic width of a piece of text like "a
b cd e", where the text is allowed to wrap at spaces, would be the
width of the whole "a b cd e" string, with no wrapping.
** `double getMinIntrinsicHeight(BoxConstraints constraints)` must
return the height that fits within the given constraints below
which making the height constraint smaller would not increase the
resulting width, or, to put it another way, the shortest height at
which the box can be rendered without failing to lay the children
out within itself.
The minimum intrinsic height of a width-in-height-out algorithm,
like English text layout, would be the height of the text at the
width that would be used given the constraints. So for instance,
given the text "hello world", if the constraints were such that it
had to wrap at the space, then the minimum intrinsic height would
be the height of two lines (and the appropriate line spacing). If
the constraints were such that it all fit on one line, then it
would be the height of one line.
** `double getMaxIntrinsicHeight(BoxConstraints constraints)` must
return the height that fits within the given constraints above
which making the height constraint larger would not decrease the
resulting width. If the height depends exclusively on the width,
and the width does not depend on the height, then
`getMinIntrinsicHeight()` and `getMaxIntrinsicHeight()` will return the
same number given the same constraints.
In the case of English text, the maximum intrinsic height is the
same as the minimum instrinsic height.
* The box must have a `performLayout()` method that encapsulates the
layout algorithm that this class represents. It is responsible for
telling the children to lay out, positioning the children, and, if
sizedByParent is false, sizing the object.
Specifically, the method must walk over the object's children, if
any, and for each one call `child.layout()` with a BoxConstraints
object as the first argument, and a second argument named
`parentUsesSize` which is set to true if the child's resulting size
will in any way influence the layout, and omitted (or set to false)
if the child's resulting size is ignored. The children's positions
(`child.parentData.position`) must then be set.
(Calling `layout()` can result in the child's own `performLayout()`
method being called recursively, if the child also needs to be laid
out. If the child's constraints haven't changed and the child is not
marked as needing layout, however, this will be skipped.)
The parent must not set a child's `size` directly. If the parent
wants to influence the child's size, it must do so via the
constraints that it passes to the child's `layout()` method.
If an object's `sizedByParent` is false, then its `performLayout()`
must also size the object (by setting `size`), otherwise, the size
must be left untouched.
* The `size` member must never be set to an infinite value.
* The box must also implement `hitTestChildren()`.
TODO(ianh): Define this better
* The box must also implement `paint()`.
TODO(ianh): Define this better
#### Using RenderProxyBox
### The Hit Testing contract
Performance rules of thumb
--------------------------
* Avoid using transforms where mere maths would be sufficient (e.g.
draw your rectangle at x,y rather than translating by x,y and
drawing it at 0,0).
* Avoid using save/restore on canvases.
Useful debugging tools
----------------------
This is a quick way to dump the entire render tree to the console every frame.
This can be quite useful in figuring out exactly what is going on when
working with the render tree.
```dart
import 'package:sky/animation.dart';
import 'package:sky/rendering.dart';
scheduler.addPersistentFrameCallback((_) {
SkyBinding.instance.debugDumpRenderTree();
});
```
Dependencies
------------
* [`package:sky/animation.dart`](../../animation.dart)
* [`package:sky/mojo`](../../mojo)
* [`package:sky/painting.dart`](../../painting.dart)
......@@ -360,7 +360,7 @@ abstract class RenderBox extends RenderObject {
assert(constraints != null);
assert(_size != null);
assert(() {
'See https://github.com/domokit/sky_engine/blob/master/sky/packages/sky/lib/src/widgets/sizing.md#user-content-unbounded-constraints';
'See https://flutter.github.io/layout/#unbounded-constraints';
return !_size.isInfinite;
});
bool result = constraints.isSatisfiedBy(_size);
......
......@@ -351,7 +351,7 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
// Flexible children can only be used when the RenderFlex box's container has a finite size.
// When the container is infinite, for example if you are in a scrollable viewport, then
// it wouldn't make any sense to have a flexible child.
assert(canFlex && 'See https://github.com/domokit/sky_engine/blob/master/sky/packages/sky/lib/src/widgets/sizing.md#user-content-flex' is String);
assert(canFlex && 'See https://flutter.github.io/layout/#flex' is String);
totalFlex += child.parentData.flex;
} else {
BoxConstraints innerConstraints;
......
Sky Widgets
===========
Sky widgets are built using a functional-reactive framework, which takes
inspiration from [React](http://facebook.github.io/react/). The central idea is
that you build your UI out of components. Components describe what their view
should look like given their current configuration and state. When a component's
state changes, the component rebuilds its description, which the framework diffs
against the previous description in order to determine the minimal changes needed
in the underlying render tree to transition from one state to the next.
Hello World
-----------
To build an application, create a subclass of `App` and instantiate it:
```dart
import 'package:sky/widgets.dart';
class HelloWorldApp extends App {
Widget build() {
return new Center(child: new Text('Hello, world!'));
}
}
void main() {
runApp(new HelloWorldApp());
}
```
An app is comprised of (and is, itself, a) widgets. The most commonly authored
widgets are, like `App`, subclasses of `Component`. A component's main job is
to implement `Widget build()` by returning newly-created instances of other
widgets. If a component builds other components, the framework will build those
components in turn until the process bottoms out in a collection of basic
widgets, such as those in `sky/widgets/basic.dart`. In the case of
`HelloWorldApp`, the `build` function simply returns a new `Text` node, which is
a basic widget representing a string of text.
Basic Widgets
-------------
Sky comes with a suite of powerful basic widgets, of which the following are
very commonly used:
* `Text`: The `Text` widget lets you create a run of styled text within your
application.
* `Row`, `Column`: These flex widgets let you create flexible layouts
in both the horizontal (`Row`) and vertical (`Column`) directions.
Its design is based on the web's flexbox layout model.
* `Container`: The `Container` widget lets you create rectangular visual
element. A container can be decorated with a `BoxDecoration`, such as a
background, a border, or a shadow. A `Container` can also have margins,
padding, and constraints applied to its size. In addition, a `Container` can
be transformed in three dimensional space using a matrix.
* `NetworkImage`: The `NetworkImage` widget lets you display an image, referenced
using a URL. The underlying image is cached, which means if several
`NetworkImage` widgets refer to the same URL, they'll share the underlying image
resource.
Below is a simple toolbar example that shows how to combine these widgets:
```dart
import 'package:sky/widgets.dart';
class MyToolBar extends Component {
Widget build() {
return new Container(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF00FFFF)
),
height: 56.0,
padding: const EdgeDims.symmetric(horizontal: 8.0),
child: new Row([
new NetworkImage(src: 'menu.png', width: 25.0, height: 25.0),
new Flexible(child: new Text('My awesome toolbar')),
new NetworkImage(src: 'search.png', width: 25.0, height: 25.0),
])
);
}
}
```
The `MyToolBar` component creates a cyan `Container` with a height of
56 device-independent pixels with an internal padding of 8 pixels,
both on the left and the right. Inside the container, `MyToolBar` uses
a `Row` layout. The middle child, the `Text` widget, is marked as
`Flexible`, which means it expands to fill any remaining available
space that hasn't been consumed by the inflexible children. You can
have multiple `Flexible` children and determine the ratio in which
they consume the available space using the `flex` argument to
`Flexible`.
To use this component, we simply create an instance of `MyToolBar` in a `build`
function:
```dart
import 'package:sky/widgets.dart';
import 'my_tool_bar.dart';
class DemoApp extends App {
Widget build() {
return new Center(child: new MyToolBar());
}
}
void main() {
runApp(new DemoApp());
}
```
Here, we've used the `Center` widget to center the toolbar within the view, both
vertically and horizontally. If we didn't center the toolbar, it would fill the
view, both vertically and horizontally, because the root widget is sized to fill
the view.
Listening to Events
-------------------
In addition to being stunningly beautiful, most applications react to user
input. The first step in building an interactive application is to listen for
input events. Let's see how that works by creating a simple button:
```dart
import 'package:sky/widgets.dart';
final BoxDecoration _decoration = new BoxDecoration(
borderRadius: 5.0,
gradient: new LinearGradient(
start: Point.origin,
end: const Point(0.0, 36.0),
colors: [ const Color(0xFFEEEEEE), const Color(0xFFCCCCCC) ]
)
);
class MyButton extends Component {
Widget build() {
return new Listener(
onGestureTap: (event) {
print('MyButton was tapped!');
},
child: new Container(
height: 36.0,
padding: const EdgeDims.all(8.0),
margin: const EdgeDims.symmetric(horizontal: 8.0),
decoration: _decoration,
child: new Center(
child: new Text('Engage')
)
)
);
}
}
```
The `Listener` widget doesn't have an visual representation but instead listens
for events bubbling through the application. When a tap gesture bubbles out from
the `Container`, the `Listener` will call its `onGestureTap` callback, in this
case printing a message to the console.
You can use `Listener` to listen for a variety of input events, including
low-level pointer events and higher-level gesture events, such as taps, scrolls,
and flings.
Generic Components
------------------
One of the most powerful features of components is the ability to pass around
references to already-built widgets and reuse them in your `build` function. For
example, we wouldn't want to define a new button component every time we wanted
a button with a novel label:
```dart
class MyButton extends Component {
MyButton({ this.child, this.onPressed });
final Widget child;
final Function onPressed;
Widget build() {
return new Listener(
onGestureTap: (_) {
if (onPressed != null)
onPressed();
},
child: new Container(
height: 36.0,
padding: const EdgeDims.all(8.0),
margin: const EdgeDims.symmetric(horizontal: 8.0),
decoration: _decoration,
child: new Center(child: child)
)
);
}
}
```
Rather than providing the button's label as a `String`, we've let the code that
uses `MyButton` provide an arbitrary `Widget` to put inside the button. For
example, we can put an elaborate layout involving text and an image inside the
button:
```dart
Widget build() {
return new MyButton(
child: new ShrinkWrapWidth(
child: new Row([
new NetworkImage(src: 'thumbs-up.png', width: 25.0, height: 25.0),
new Container(
padding: const EdgeDims.only(left: 10.0),
child: new Text('Thumbs up')
)
])
)
);
}
```
State
-----
By default, components are stateless. Components usually receive
arguments from their parent component in their constructor, which they typically
store in `final` member variables. When a component is asked to `build`, it uses
these stored values to derive new arguments for the subcomponents it creates.
For example, the generic version of `MyButton` above follows this pattern. In
this way, state naturally flows "down" the component hierachy.
Some components, however, have mutable state that represents the transient state
of that part of the user interface. For example, consider a dialog widget with
a checkbox. While the dialog is open, the user might check and uncheck the
checkbox several times before closing the dialog and committing the final value
of the checkbox to the underlying application data model.
```dart
class MyCheckbox extends Component {
MyCheckbox({ this.value, this.onChanged });
final bool value;
final Function onChanged;
Widget build() {
Color color = value ? const Color(0xFF00FF00) : const Color(0xFF0000FF);
return new Listener(
onGestureTap: (_) => onChanged(!value),
child: new Container(
height: 25.0,
width: 25.0,
decoration: new BoxDecoration(backgroundColor: color)
)
);
}
}
class MyDialog extends StatefulComponent {
MyDialog({ this.onDismissed });
Function onDismissed;
bool _checkboxValue = false;
void _handleCheckboxValueChanged(bool value) {
setState(() {
_checkboxValue = value;
});
}
void syncConstructorArguments(MyDialog source) {
onDismissed = source.onDismissed;
}
Widget build() {
return new Row([
new MyCheckbox(
value: _checkboxValue,
onChanged: _handleCheckboxValueChanged
),
new MyButton(
onPressed: () => onDismissed(_checkboxValue),
child: new Text("Save")
),
],
justifyContent: FlexJustifyContent.center);
}
}
```
The `MyCheckbox` component follows the pattern for stateless components. It
stores the values it receives in its constructor in `final` member variables,
which it then uses during its `build` function. Notice that when the user taps
on the checkbox, the checkbox itself doesn't use `value`. Instead, the checkbox
calls a function it received from its parent component. This pattern lets you
store state higher in the component hierarchy, which causes the state to persist
for longer periods of time. In the extreme, the state stored on the `App`
component persists for the lifetime of the application.
The `MyDialog` component is more complicated because it is a stateful component.
Let's walk through the differences in `MyDialog` caused by its being stateful:
* `MyDialog` extends StatefulComponent instead of Component.
* `MyDialog` has non-`final` member variables. Over the lifetime of the dialog,
we'll need to modify the values of these member variables, which means we
cannot mark them `final`.
* `MyDialog` has private member variables. By convention, components store
values they receive from their parent in public member variables and store
their own internal, transient state in private member variables. There's no
requirement to follow this convention, but we've found that it helps keep us
organized.
* Whenever `MyDialog` modifies its transient state, the dialog does so inside
a `setState` callback. Using `setState` is important because it marks the
component as dirty and schedules it to be rebuilt. If a component modifies
its transient state outside of a `setState` callback, the framework won't
know that the component has changed state and might not call the component's
`build` function, which means the user interface might not update to reflect
the changed state.
* `MyDialog` implements the `syncConstructorArguments` member function. To
understand `syncConstructorArguments`, we'll need to dive a bit deeper into
how the `build` function is used by the framework.
A component's `build` function returns a tree of widgets that represent a
"virtual" description of its appearance. The first time the framework calls
`build`, the framework walks this description and creates a "physical" tree
of `RenderObjects` that matches the description. When the framework calls
`build` again, the component still returns a fresh description of its
appearence, but this time the framework compares the new description with the
previous description and makes the minimal modifications to the underlying
`RenderObjects` to make them match the new description.
In this process, old stateless components are discarded and the new stateless
components created by the parent component are retained in the widget
hierchy. Old _stateful_ components, however, cannot simply be discarded
because they contain state that needs to be preserved. Instead, the old
stateful components are retained in the widget hierarchy and asked to
`syncConstructorArguments` with the new instance of the component created by
the parent in its `build` function.
Without `syncConstructorArguments`, the new values the parent component
passed to the `MyDialog` constructor in the parent's `build` function would
be lost because they would be stored only as member variables on the new
instance of the component, which is not retained in the component hiearchy.
Therefore, the `syncConstructorArguments` function in a component should
update `this` to account for the new values the parent passed to `source`
because `source` is the authorative source of those values.
By convention, components typically store the values they receive from their
parents in public member variables and their own internal state in private
member variables. Therefore, a typical `syncConstructorArguments`
implementation will copy the public, but not the private, member variables
from `source`. When following this convention, there is no need to copy over
the private member variables because those represent the internal state of
the object and `this` is the authoritative source of that state.
When implementing a `StatefulComponent`, make sure to call
`super.syncConstructorArguments(source)` from within your
`syncConstructorArguments()` method, unless you are extending
`StatefulComponent` directly.
Finally, when the user taps on the "Save" button, `MyDialog` follows the same
pattern as `MyCheckbox` and calls a function passed in by its parent component
to return the final value of the checkbox up the hierarchy.
didMount and didUnmount
-----------------------
When a component is inserted into the widget tree, the framework calls the
`didMount` function on the component. When a component is removed from the
widget tree, the framework calls the `didUnmount` function on the component.
In some situations, a component that has been unmounted might again be mounted.
For example, a stateful component might receive a pre-built component from its
parent (similar to `child` from the `MyButton` example above) that the stateful
component might incorporate, then not incorporate, and then later incorporate
again in the widget tree it builds, according to its changing state.
Typically, a stateful component will override `didMount` to initialize any
non-trivial internal state. Initializing internal state in `didMount` is more
efficient (and less error-prone) than initializing that state during the
component's constructor because parent executes the component's constructor each
time the parent rebuilds even though the framework mounts only the first
instance into the widget hierarchy. (Instead of mounting later instances, the
framework passes them to the original instance in `syncConstructorArguments` so
that the first instance of the component can incorporate the values passed by
the parent to the component's constructor.)
Components often override `didUnmount` to release resources or to cancel
subscriptions to event streams from outside the widget hierachy. When overriding
either `didMount` or `didUnmount`, a component should call its superclass's
`didMount` or `didUnmount` function.
initState
---------
The framework calls the `initState` function on stateful components before
building them. The default implementation of initState does nothing. If your
component requires non-trivial work to initialize its state, you should
override initState and do it there rather than doing it in the stateful
component's constructor. If the component doesn't need to be built (for
example, if it was constructed just to have its fields synchronized with
an existing stateful component) you'll avoid unnecessary work. Also, some
operations that involve interacting with the widget hierarchy cannot be
done in a component's constructor.
When overriding `initState`, a component should call its superclass's
`initState` function.
Keys
----
If a component requires fine-grained control over which widgets sync with each
other, the component can assign keys to the widgets it builds. Without keys, the
framework matches widgets in the current and previous build according to their
`runtimeType` and the order in which they appear. With keys, the framework
requires that the two widgets have the same `key` as well as the same
`runtimeType`.
Keys are most useful in components that build many instances of the same type of
widget. For example, consider an infinite list component that builds just enough
copies of a particular widget to fill its visible region:
* Without keys, the first entry in the current build would always sync with the
first entry in the previous build, even if, semantically, the first entry in
the list just scrolled off screen and is no longer visible in the viewport.
* By assigning each entry in the list a "semantic" key, the infinite list can
be more efficient because the framework will sync entries with matching
semantic keys and therefore similiar (or identical) visual appearances.
Moreover, syncing the entries semantically means that state retained in
stateful subcomponents will remain attached to the same semantic entry rather
than the entry in the same numerical position in the viewport.
Widgets for Applications
------------------------
There are some widgets that do not correspond to on-screen pixels but that are
nonetheless useful for building applications.
* `Theme`: Takes a [ThemeData](../theme/README.md) object in its `data` argument, to configure the Material Design theme of the rest of the application (as given in the `child` argument).
* `TaskDescription`: Takes a `label` that names the application for the purpose of the Android task switcher. The colour of the application as used in the system UI is taken from the current `Theme`.
* `Navigator`: Takes a single argument, which must be a long-lived instance of `NavigatorState`. This object choreographs how the application goes from screen to screen (e.g. from the main screen to a settings screen), as well as modal dialogs, drawer state, and anything else that responds to the system "back" button. By convention the `NavigatorState` object is a private member variable of the class that inherits from `App`, initialized in the `initState()` function. The `NavigatorState` constructor takes a list of `Route` objects, each of which takes a `name` argument giving a path to identify the window (e.g. "/" for the home screen, "/settings" for the settings screen, etc), and a `builder` argument that takes a method which itself takes a `navigator` argument and a `route` argument and returns a `Widget` representing that screen.
Putting this together, a basic application becomes:
```
dart
import 'package:sky/widgets.dart';
class DemoApp extends App {
NavigationState _state;
void initState() {
_state = new NavigationState([
new Route(
name: '/',
builder: (navigator, route) {
return new Center(child: new Text('Hello Slightly More Elaborate World'));
}
)
]);
super.initState();
}
Widget build() {
return new Theme(
data: new ThemeData(
brightness: ThemeBrightness.light
),
child: new TaskDescription(
label: 'Sky Demo',
child: new Navigator(_state)
)
);
}
}
void main() {
runApp(new DemoApp());
}
```
Useful debugging tools
----------------------
This is a quick way to dump the entire widget tree to the console.
This can be quite useful in figuring out exactly what is going on when
working with the widgets system. For this to work, you have to have
launched your app with `runApp()`.
```dart
debugDumpApp();
```
Dependencies
------------
* `package:vector_math`
* [`package:sky/animation.dart`](../../animation.dart)
* [`package:sky/painting.dart`](../../painting.dart)
* [`package:sky/rendering.dart`](../../rendering.dart)
* [`package:sky/theme`](../../theme)
Sizing in Sky
=============
Background
----------
In Sky, widgets are rendered by render boxes. Render boxes are given
constraints by their parent, and size themselves within those
constraints. Constraints consist of minimum and maximum widths and
heights; sizes consist of a specific width and height.
Generally, there are three kinds of boxes, in terms of how they handle
their constraints:
- Those that try to be as big as possible.
For example, the boxes used by `Center` and `Block`.
- Those that try to be the same size as their children.
For example, the boxes used by `Transform` and `Opacity`.
- Those that try to be a particular size.
For example, the boxes used by `Image` and `Text`.
Some widgets, for example `Container`, vary from type to type based on
their constructor arguments. In the case of `Container`, it defaults
to trying to be as big as possible, but if you give it a `width`, for
instance, it tries to honor that and be that particular size.
Others, for example `Row` and `Column` (flex boxes) vary based on the
constraints they are given, as described below in the "Flex" section.
The constraints are sometimes "tight", meaning that they leave no room
for the render box to decide on a size (e.g. if the minimum and
maximum width are the same, it is said to have a tight width). The
main example of this is the `App` widget, which is contained by the
`RenderView` class: the box used by the child returned by the
application's `build` function is given a constraint that forces it to
exactly fill the application's content area (typically, the entire
screen). Many of the boxes in Sky, especially those that just take a
single child, will pass their constraint on to their children. This
means that if you nest a bunch of boxes inside each other at the root
of your application's render tree, they'll all exactly fit in each
other, forced by these tight constraints.
Some boxes _loosen_ the constraints, meaning the maximum is maintained
but the minimum is removed. For example, `Center`.
Unbounded constraints
---------------------
In certain situations, the constraint that is given to a box will be
_unbounded_, or infinite. This means that either the maximum width or
the maximum height is set to `double.INFINITY`.
A box that tries to be as big as possible won't function usefully when
given an unbounded constraint, and in checked mode, will assert with a
message saying `!_size.isInfinite` and a string that points to this
file.
The most common cases where a render box finds itself with unbounded
constraints are within flex boxes (`Row` and `Column`), and **within
scrollable regions** (mainly `Block`, `ScollableList<T>`, and
`ScrollableMixedWidgetList`).
In particular, `Block` tries to expand to fit the space available in
its cross-direction (i.e. if it's a vertically-scrolling block, it
will try to be as wide as its parent). If you nest a vertically
scrolling `Block` inside a horizontally scrolling `Block`, the inner
one will try to be as wide as possible, which is infinitely wide,
since the outer one is scrollable in that direction.
Flex
----
Flex boxes themselves (`Row` and `Column`) behave differently based on
whether they are in a bounded constraints or unbounded constraints in
their given direction.
In bounded constraints, they try to be as big as possible in that
direction.
In unbounded constraints, they try to fit their children in that
direction. In this case, you cannot set `flex` on the children to
anything other than 0 (the default). In the widget hierarchy, this
means that you cannot use `Flexible` when the flex box is inside
another flex box or inside a scrollable. If you do, you'll get an
assert that `canFlex` is not true, pointing you at this section.
In the _cross_ direction, i.e. in their width for `Column` (vertical
flex) and in their height for `Row` (horizontal flex), they must never
be unbounded, otherwise they would not be able to reasonably align
their children.
Sky Widgets: Basic
==================
This document describes the basic widgets available in Sky. These widgets are
general-purpose and don't offer an opinion about the visual style of your app.
Container
---------
`Container` is a general-purpose widget that combines several basic widgets in
order to make them easier to use.
- `BoxDecoration decoration` Draw the given decoration around this container.
- `double width` Forces the container to have the given width.
- `double height` Force the container to have the given height.
- `EdgeDims margin` Surrounds the container (i.e., outside the container's
decoration) on the top, right, bottom, and left with the given amount of
space.
- `EdgeDims padding` Surrounds the container's child (i.e., inside the
container's decoration) on the top, right, bottom, and left with the given
amount of space.
- `Matrix4 transform` Apply the given matrix before painting the container.
- `BoxConstraints constraints` Force the width and height of the container to
respect the given constraints.
Layout models
-------------
There are two _flex_ layout models:
- `Row`: Layout a list of child widgets in the horizontal direction.
- `Column': Layout a list of child widgets in the vertical direction.
The direction along which the widgets are laid out is called the
*main* direction and the other axis is called the *cross* direction.
These flex widgets size themselves to the maximum size permitted by
its parent, unless that would be infinite size, in which case they
shrink-wrap their children. For details, see [flex.md](flex.md).
Each child of a flex widget is either *flexible* or *inflexible*.
The flex first lays out its inflexible children and subtracts their
total length along the main direction to determine how much free space
is available. The flex then divides this free space among the flexible
children in a ratio determined by their `flex` properties.
The `alignItems` property determines how children are positioned in
the cross direction. The `justifyContent` property determines how the
remaining free space (if any) in the main direction is allocated.
- `Flexible`: Mark this child as being flexible with the given `flex` ratio.
There is also a stacking layout model:
- `Stack`: Layout a list of child widgets on top of each other from back to
front. Each child of a `Stack` widget is either *positioned* or
*non-positioned*. The stack sizes itself to the contain all the
non-positioned children, which are located at the top-left corner of the
stack. The *positioned* children are then located relative to the stack
according to their `top`, `right`, `bottom`, and `left` properties.
- `Positioned`: Mark this child as *positioned*. If the `top` property is
non-null, the top edge of this child will be positioned `top` layout units
from the top of the stack widget. The `right`, `bottom`, and `right`
properties work analogously. Note that if the both the `top` and `bottom`
properties are non-null, then the child will be forced to have exactly the
height required to satisfy both constraints. Similarly, setting the
`right` and `left` properties to non-null values will force the child to
have a particular width.
Positioning and sizing
----------------------
- `Padding` Surround the child with empty space on the top, right, bottom, and
left according to the given `EdgeDims`.
- `Center` Center the child widget within the space occupied by this widget.
- `SizedBox` Force the child widget to have a particular `width` or `height`
(or both).
- `ConstrainedBox` Apply the given `BoxConstraints` to the child widget as
additional constraints during layout. This widget is a generalization of
`SizedBox`.
- `AspectRatio` Force the child widget's width and height to have the given
`aspectRatio`, expressed as a ratio of width to height.
- `Transform` Apply the given matrix to the child before painting the child.
This widget is useful for adjusting the visual size and position of a widget
without affecting layout.
- `Viewport` Layout the child widget at a larger size than fits in this widget
and render only the portion of the child that is visually contained by this
widget. When rendering, add `offset` to the child's vertical position to
control which part of the child is visible through the viewport.
TODO(abarth): Add support for horizontal viewporting.
- `SizeObserver` Whenever the child widget changes size, this widget calls the
`callback`. Warning: If the callback changes state that affects the size of
the child widget, it is possible to create an infinite loop.
- `ShrinkWrapWidth` Force the child widget to have a width equal to its max
intrinsic width. TODO(abarth): Add a link to the definition of max intrinsic
width. Optionally, round up the child widget's width or height (or both) to
a multiple of `stepWidth` or `stepHeight`, respectively. Note: The layout
performed by `ShrinkWrapWidth` is relatively expensive and should be used
sparingly.
- `Baseline` If the child widget has a `TextBaseline` of the given
`baselineType`, position the child such that its baseline is at `baseline`
layout units from the top of this widget.
Painting effects
----------------
- `Opacity` Adjusts the opacity of the child widget, making the child partially
transparent. The amount of transparency is controlled by `opacity`, with 0.0
0.0 is fully transparent and 1.0 is fully opaque.
- `ClipRect` Apply a rectangular clip to the child widget. The dimensions of
the clip match the dimensions of the child.
- `ClipRRect` Apply a rounded-rect clip the child widget. The bounds of the
clip match the bounds of the child widget with `xRadius` and `yRadius`
controlling the x and y radius of the rounded corner, respectively.
- `ClipOval` Apply an oval clip to the child widget. The oval will be
axis-aligned, with its horizontal and vertical bounds matching the bounds of
the child widget.
- `DecoratedBox` Draw the given `BoxDecoration` surrounding the child widget.
- `ColorFilter` Applies a color filter to the child widget, for example to
tint the child a given color.
- `CustomPaint` Calls `callback` during the paint phase with the current
`Canvas` and `Size`. The widget occupies the region of the canvas starting at
the origin (i.e., `x = 0.0` and `y = 0.0`) and of the given size (i.e.,
`x = size.width` and `y = size.height`).
Use the `token` to invalidate the painting. As long as the any new `token`
is `operator==` the current `token`, the `CustomPaint` widget is permitted
to retain a recording of the painting produced by the previous `callback`
call.
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