mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Also, give some more visual structure to the dependency declarations. TBR=ianh@google.com Review URL: https://codereview.chromium.org/1189343002.
424 lines
16 KiB
Markdown
424 lines
16 KiB
Markdown
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 minial 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/basic.dart';
|
|
|
|
class HelloWorldApp extends App {
|
|
Widget build() {
|
|
return 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.
|
|
|
|
* `Flex`: The `Flex` widget lets you create flexible layouts in both the
|
|
horizontal and vertical direction. Its design is based on the web's flexbox
|
|
layout model. You can also use the simpler `Block` widget to create vertical
|
|
layouts of inflexible items.
|
|
|
|
* `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.
|
|
|
|
* `Image`: The `Image` widget lets you display an image, referenced using a
|
|
URL. The underlying image is cached, which means if several `Image` 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/basic.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 Flex([
|
|
new Image(src: 'menu.png', size: const Size(25.0, 25.0)),
|
|
new Flexible(child: new Text('My awesome toolbar')),
|
|
new Image(src: 'search.png', size: const Size(25.0, 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 `Flex` layout
|
|
in the (default) horizontal direction. 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/basic.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/basic.dart';
|
|
|
|
final BoxDecoration _decoration = new BoxDecoration(
|
|
borderRadius: 5.0,
|
|
gradient: new LinearGradient(
|
|
endPoints: [ Point.origin, 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 Flex([
|
|
new Image(src: 'thumbs-up.png', size: const Size(25.0, 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 Component {
|
|
MyDialog({ this.onDismissed }) : super(stateful: true);
|
|
|
|
Function onDismissed;
|
|
bool _checkboxValue = false;
|
|
|
|
void _handleCheckboxValueChanged(bool value) {
|
|
setState(() {
|
|
_checkboxValue = value;
|
|
});
|
|
}
|
|
|
|
void syncFields(MyDialog source) {
|
|
onDismissed = source.onDismissed;
|
|
}
|
|
|
|
Widget build() {
|
|
return new Flex([
|
|
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` passes `stateful: true` to the `Component` constructor, marking
|
|
the component stateful.
|
|
|
|
* `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 `syncFields` member function. To understand
|
|
`syncFields`, 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 hiearchy and asked to
|
|
`syncFields` with the new instance of the component created by the parent in
|
|
its `build` function.
|
|
|
|
Without `syncFields`, 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
|
|
`syncFields` 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 `syncFields` 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.
|
|
|
|
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 heiarchy. (Instead of mounting later instances, the
|
|
framework passes them to the original instance in `syncFields` 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.
|
|
|
|
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.
|
|
|
|
Dependencies
|
|
------------
|
|
|
|
* `package:vector_math`
|
|
* [`package:sky/animation`](../animation)
|
|
* [`package:sky/base`](../base)
|
|
* [`package:sky/painting`](../painting)
|
|
* [`package:sky/rendering`](../rendering)
|
|
* [`package:sky/theme`](../theme)
|