Instruments

Flutter 2015

Sky widgets are built using a functional-reactive framework, which takes
inspiration from 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:

1
2
3
4
5
6
7
8
9
10
11
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:

Below is a simple toolbar example that shows how to combine these widgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
  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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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:

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:

Widgets for Applications

There are some widgets that do not correspond to on-screen pixels but that are
nonetheless useful for building applications.

Putting this together, a basic application becomes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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().

1
debugDumpApp();

Basic Usage

Creating a Single-page Application with Vue.js + vue-router is dead simple. With Vue.js, we are already breaking our application into components. When adding vue-router to the mix, all we need to do is map our components to the routes and let vue-router know where to render them. Here’s a basic example:

HTML

1
2
3
4
5
6
7
8
9
10
<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- v-link directive -->
    <a v-link="/foo">GoFoo</a>
    <a v-link="/bar">GoBar</a>
  </p>
  <!-- route outlet -->
  <router-view></router-view>
</div>

JavaScript

The router needs a root component to render, Because we are using the HTML as the app template. for demo purposes, we will just use an empty one. You can pass in additional options here, but let’s keep it simple for now.Each route should map to a component, we’ll talk about nested routes later.router.start will create an instance of App and mount to the element matching the selector #app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Define some components
var Foo = Vue.extend({
  template: "<p>is foo!</p>",
});

var Bar = Vue.extend({
  template: "<p>is bar!</p>",
});

var App = Vue.extend({});

// create a router instance.
var router = new VueRouter();

// Define some routes.
router.map({
  "/foo": {
    component: Foo,
  },
  "/bar": {
    component: Bar,
  },
});

// now we can start the app!
router.start(App, "#app");

You can also checkout this example live.

Nested Routes

Mapping nested routes to nested components is a common need, and it is also very simple with vue-router.

Suppose we have the following app:

1
2
3
<div id="app">
  <router-view></router-view>
</div>

The <router-view> here is a top-level outlet. It renders the component matched by a top level route:
Foo is rendered when /foo is matched

1
2
3
4
5
router.map({
  "/foo": {
    component: Foo,
  },
});

Similarly, a rendered component can also contain its own, nested <router-view>. For example, if we add one inside the Foo component’s template:

1
2
3
4
5
6
7
8
9
// router-view is nest outlet
var Foo = Vue.extend({
  template:
    '<div class="foo">'+
    "<h2>This is Foo!</h2>"+
    "<router-view>"+
    "</router-view>"+
    "</div>",
});

To render components into this nested outlet, we need to update our route config:
Now, with the above configuration, when you visit /foo, nothing will be rendered inside Foo‘s outlet, because no sub route is matched. Maybe you do want to render something there. In such case you can provide a / subroute in this case: Bar will be rendered inside Foo’s <router-view>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// subRoutes map under /foo 
router.map({
  "/foo": {
    component: Foo,
    subRoutes: {
      "/bar": {
        // /foo/bar is show
        component: Bar,
      },
      "/baz": {
        component: Baz,
      },
    },
  },
});

This component will be rendered into Foo’s , when /foo is matched. Using an inline component definition here for convenience.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// HTML String
var html = "<p>sub view</p>"
router.map({
  "/foo": {
    component: Foo,
    subRoutes: {
      "/": {
        component: {
          template: html,
        },
      },
      // other sub routes...
    },
  },
});

A working demo of this example can be found here.

Route Object & Route Matching

Vue-router supports matching paths that contain dynamic segments, star segments and query strings. All these information of a parsed route will be accessible on the exposed Route Context Objects (we will just call them “route” objects from now on). The route object will be injected into every component in a vue-router-enabled app as this.$route, and will be updated whenever a route transition is performed.

A route object exposes the following properties:

Using in Templates

You can directly bind to the $route object inside your component templates. For example:

1
2
3
4
<div>
  <p>Current route: </p>
  <p>Current params: ""</p>
</div>

Dynamic Segments

Dynamic segments can be defined in the form of path segments with a leading colon, e.g. in user/:username, :username is the dynamic segment. It will match paths like /user/foo or /user/bar. When a path containing a dynamic segment is matched, the dynamic segments will be available inside $route.params.

Example Usage:

1
2
3
4
5
6
7
8
9
10
var string = ''
router.map({
  '/user/:username': {
    component: {
      template: '<p>user is '+
      code +
      '</p>'
    }
  }
})

A path can contain multiple dynamic segments, and each of them will be stored as a key/value pair in $route.params.

Route Options

There are a number of options you can use to customize the router behavior when creating a router instance.

hashbang

history

abstract

root

linkActiveClass

saveScrollPosition

transitionOnLoad

suppressTransitionError

router-view

The <router-view> element is used as outlets for rendering matched components. It is based upon Vue’s dynamic component system, and therefore inherits many features from a normal dyanmic component:

However, there are also a few limitations:

v-link

You should use the v-link directive for handling navigations inside a vue-router-enabled app for the following reasons:

Elements with v-link will automatically get corresponding class names when the current path matches its v-link URL:

The active link class name can be configured with the activeLinkClass option when creating the router instance. The exact match class simply appends -exact postfix to the provided class name.

Additional Notes

To better understand the pipeline of a route transition, let’s imagine we have a router-enabled app, already rendered with three nested <router-view> with the path /a/b/c:

And then, the user navigates to a new path, /a/d/e, which requires us to update our rendered component tree to a new one:

How would we go about that? There are a few things we need to do here:

  1. We can potentially reuse component A, because it remains the same in the post-transition component tree.

  2. We need to deactivate and remove component B and C.

  3. We need to create and activate component D and E.

  4. Before we actually perform step 2 & 3, we also want to make sure this transition is valid - that is, to make sure that all components involved in this transition can be deactivated/activated as desired.

Transition Phases

With these in mind, we can divide a route transition pipeline into three phases:

  1. Reusability phase:

    Check if any component in the current view hierarchy can be reused in the new one. This is done by comparing the two component trees, find out common components, and then check their reusability. By default, every component is reusable unless configured otherwise.

    reusability phase

  2. Validation phase:

    Check if all current components can be deactivated, and if all new components can be activated. This is by checking and calling their canDeactivate and canActivate route config hooks.

    validation phase

    Note the canDeactivate check bubbles bottom-up, while the canActivate check is top-down.

    Any of these hooks can potentially abort the transition. If a transition is aborted during the validation phase, the router preserve current app state and restore the previous path.

  3. Activation phase:

    Once all validation hooks have been called and none of them aborts the transition, the transition is now said to be valid. The router will now deactivate current components and activate new components.

    activation phase

    These hooks are called in the same order of the validation hooks, but their purpose is to give you the chance to do cleanup / preparation work before the visible component switching is executed. The interface will not update until all of the affected components’ deactivate and activate hooks have resolved.

We will talk about transition hooks in detail next.

Transition Hooks

A <router-view> component involved in a transition can control / react to the transition by implementing appropriate transition pipeline hooks. These hooks include:

You can implement these hooks under your component’s route option:

1
2
3
4
5
6
7
8
9
10
11
12
13
Vue.component('hook-demo', {
  //... other options
  route: {
    activate:function(tr) {
      //hook-demo activated!
      tr.next()
    },
    deactivate:function(tr) {
      //hook-demo deactivated!
      tr.next()
    }
  }
})

The Transition Object

Each transition hook will receive a transition object as the only argument. The transition object exposes the following properties & methods:

All transition hooks are considered asynchronous by default. In order to signal the transition to progress, you have three options:

  1. Explicitly call one of next, abort or redirect.

  2. Return a Promise. Details below.

  3. For validation hooks (canActivate and canDeactivate), you can synchronously return a Boolean value.

Returning Promise in Hooks

When you return a Promise in a transition hook, transition.next will be called for you when the Primise resolves. If the Promise is rejected during validation phase, it will call transition.abort; if it is rejected during activation phase, it will call transition.next.

For validation hooks (canActivate and canDeactivate), if the Promise’s resolved value is falsy, it will also abort the transition.

If a rejected promise has an uncaught error, it will be thrown unless you suppress it with the suppressTransitionError option when creating the router.Assuming the service returns a Promise that, set the data once it arrives.The component will not display until this is done.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// inside component definition

var fun = authenticationService
var api = fun.isLoggedIn
route: {
  canActivate:function() {
    // resolve `true`or`false`
    return api()
  },
  activate:function(tr) {
    var obj = tr.to
    var params = obj?.params
    var id = params.messageId
    return messageService
      .fetch(id)
      .then((message) => {
        this.message = message
      })
  }
}

We are asynchronously fetching data in the activate hook here just for the sake of an example; Note that we also have the data hook which is in general more appropriate for this purpose.
TIP: if you are using ES6 you can use argument destructuring to make your hooks cleaner:

1
2
3
4
5
6
route: {
  acitvate ({ next }) {
    // when done:
    next()
  }
}

Check out the advanced example in the vue-router repo.