Port stocks to fn3 and introduce an App component.
diff --git a/examples/stocks/lib/main.dart b/examples/stocks/lib/main.dart
index a70739b..aaacab0 100644
--- a/examples/stocks/lib/main.dart
+++ b/examples/stocks/lib/main.dart
@@ -10,7 +10,8 @@
 
 import 'package:sky/animation.dart';
 import 'package:sky/material.dart';
-import 'package:sky/widgets.dart';
+import 'package:sky/painting.dart';
+import 'package:sky/src/fn3.dart';
 
 import 'stock_data.dart';
 
@@ -22,53 +23,16 @@
 part 'stock_settings.dart';
 part 'stock_types.dart';
 
-class StocksApp extends App {
+class StocksApp extends StatefulComponent {
+  StocksAppState createState() => new StocksAppState();
+}
 
-  NavigationState _navigationState;
-
-  void initState() {
-    _navigationState = new NavigationState([
-      new Route(
-        name: '/',
-        builder: (navigator, route) => new StockHome(navigator, _stocks, optimismSetting, modeUpdater)
-      ),
-      new Route(
-        name: '/settings',
-        builder: (navigator, route) => new StockSettings(navigator, optimismSetting, backupSetting, settingsUpdater)
-      ),
-    ]);
-    super.initState();
-  }
-
-  void onBack() {
-    if (_navigationState.hasPrevious()) {
-      setState(() {
-        _navigationState.pop();
-      });
-    } else {
-      super.onBack();
-    }
-  }
-
-  StockMode optimismSetting = StockMode.optimistic;
-  BackupMode backupSetting = BackupMode.disabled;
-  void modeUpdater(StockMode optimism) {
-    setState(() {
-      optimismSetting = optimism;
-    });
-  }
-  void settingsUpdater({ StockMode optimism, BackupMode backup }) {
-    setState(() {
-      if (optimism != null)
-        optimismSetting = optimism;
-      if (backup != null)
-        backupSetting = backup;
-    });
-  }
+class StocksAppState extends State<StocksApp> {
 
   final List<Stock> _stocks = [];
-  void didMount() {
-    super.didMount();
+
+  void initState(BuildContext context) {
+    super.initState(context);
     new StockDataFetcher((StockData data) {
       setState(() {
         data.appendTo(_stocks);
@@ -76,32 +40,47 @@
     });
   }
 
-  Widget build() {
+  StockMode _optimismSetting = StockMode.optimistic;
+  BackupMode _backupSetting = BackupMode.disabled;
+  void modeUpdater(StockMode optimism) {
+    setState(() {
+      _optimismSetting = optimism;
+    });
+  }
+  void settingsUpdater({ StockMode optimism, BackupMode backup }) {
+    setState(() {
+      if (optimism != null)
+        _optimismSetting = optimism;
+      if (backup != null)
+        _backupSetting = backup;
+    });
+  }
 
-    ThemeData theme;
-    if (optimismSetting == StockMode.optimistic) {
-      theme = new ThemeData(
-        brightness: ThemeBrightness.light,
-        primarySwatch: Colors.purple
-      );
-    } else {
-      theme = new ThemeData(
-        brightness: ThemeBrightness.dark,
-        accentColor: Colors.redAccent[200]
-      );
+  ThemeData get theme {
+    switch (_optimismSetting) {
+      case StockMode.optimistic:
+        return new ThemeData(
+          brightness: ThemeBrightness.light,
+          primarySwatch: Colors.purple
+        );
+      case StockMode.pessimistic:
+        return new ThemeData(
+          brightness: ThemeBrightness.dark,
+          accentColor: Colors.redAccent[200]
+        );
     }
+  }
 
-    return new Theme(
-      data: theme,
-        child: new DefaultTextStyle(
-          style: Typography.error, // if you see this, you've forgotten to correctly configure the text style!
-          child: new Title(
-            title: 'Stocks',
-            child: new Navigator(_navigationState)
-          )
-        )
-     );
-   }
+  Widget build(BuildContext context) {
+    return new App(
+      title: 'Stocks',
+      theme: theme,
+      routes: <String, RouteBuilder>{
+         '/':         (navigator, route) => new StockHome(navigator, _stocks, _optimismSetting, modeUpdater),
+         '/settings': (navigator, route) => new StockSettings(navigator, _optimismSetting, _backupSetting, settingsUpdater)
+      }
+    );
+  }
 }
 
 void main() {
diff --git a/examples/stocks/lib/stock_arrow.dart b/examples/stocks/lib/stock_arrow.dart
index 394eda3..a1f56b5 100644
--- a/examples/stocks/lib/stock_arrow.dart
+++ b/examples/stocks/lib/stock_arrow.dart
@@ -4,8 +4,7 @@
 
 part of stocks;
 
-class StockArrow extends Component {
-
+class StockArrow extends StatelessComponent {
   StockArrow({ Key key, this.percentChange }) : super(key: key);
 
   final double percentChange;
@@ -22,7 +21,7 @@
     return Colors.red[_colorIndexForPercentChange(percentChange)];
   }
 
-  Widget build() {
+  Widget build(BuildContext context) {
     // TODO(jackson): This should change colors with the theme
     Color color = _colorForPercentChange(percentChange);
     const double kSize = 40.0;
@@ -65,5 +64,4 @@
       margin: const EdgeDims.symmetric(horizontal: 5.0)
     );
   }
-
 }
diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart
index c85dd54..ee47f6e 100644
--- a/examples/stocks/lib/stock_home.dart
+++ b/examples/stocks/lib/stock_home.dart
@@ -9,20 +9,17 @@
 const Duration _kSnackbarSlideDuration = const Duration(milliseconds: 200);
 
 class StockHome extends StatefulComponent {
-
   StockHome(this.navigator, this.stocks, this.stockMode, this.modeUpdater);
 
-  Navigator navigator;
-  List<Stock> stocks;
-  StockMode stockMode;
-  ModeUpdater modeUpdater;
+  final NavigatorState navigator;
+  final List<Stock> stocks;
+  final StockMode stockMode;
+  final ModeUpdater modeUpdater;
 
-  void syncConstructorArguments(StockHome source) {
-    navigator = source.navigator;
-    stocks = source.stocks;
-    stockMode = source.stockMode;
-    modeUpdater = source.modeUpdater;
-  }
+  StockHomeState createState() => new StockHomeState();
+}
+
+class StockHomeState extends State<StockHome> {
 
   bool _isSearching = false;
   String _searchQuery;
@@ -31,7 +28,7 @@
   bool _isSnackBarShowing = false;
 
   void _handleSearchBegin() {
-    navigator.pushState(this, (_) {
+    config.navigator.pushState(this, (_) {
       setState(() {
         _isSearching = false;
         _searchQuery = null;
@@ -43,9 +40,9 @@
   }
 
   void _handleSearchEnd() {
-    assert(navigator.currentRoute is RouteState);
-    assert((navigator.currentRoute as RouteState).owner == this); // TODO(ianh): remove cast once analyzer is cleverer
-    navigator.pop();
+    assert(config.navigator.currentRoute is RouteState);
+    assert((config.navigator.currentRoute as RouteState).owner == this); // TODO(ianh): remove cast once analyzer is cleverer
+    config.navigator.pop();
     setState(() {
       _isSearching = false;
       _searchQuery = null;
@@ -82,15 +79,12 @@
   }
 
   void _handleStockModeChange(StockMode value) {
-    setState(() {
-      stockMode = value;
-    });
-    if (modeUpdater != null)
-      modeUpdater(value);
+    if (config.modeUpdater != null)
+      config.modeUpdater(value);
   }
 
   void _handleMenuShow() {
-    showStockMenu(navigator,
+    showStockMenu(config.navigator,
       autorefresh: _autorefresh,
       onAutorefreshChanged: _handleAutorefreshChanged
     );
@@ -104,7 +98,7 @@
       level: 3,
       showing: _drawerShowing,
       onDismissed: _handleDrawerDismissed,
-      navigator: navigator,
+      navigator: config.navigator,
       children: [
         new DrawerHeader(child: new Text('Stocks')),
         new DrawerItem(
@@ -122,7 +116,7 @@
           onPressed: () => _handleStockModeChange(StockMode.optimistic),
           child: new Row([
             new Flexible(child: new Text('Optimistic')),
-            new Radio(value: StockMode.optimistic, groupValue: stockMode, onChanged: _handleStockModeChange)
+            new Radio(value: StockMode.optimistic, groupValue: config.stockMode, onChanged: _handleStockModeChange)
           ])
         ),
         new DrawerItem(
@@ -130,7 +124,7 @@
           onPressed: () => _handleStockModeChange(StockMode.pessimistic),
           child: new Row([
             new Flexible(child: new Text('Pessimistic')),
-            new Radio(value: StockMode.pessimistic, groupValue: stockMode, onChanged: _handleStockModeChange)
+            new Radio(value: StockMode.pessimistic, groupValue: config.stockMode, onChanged: _handleStockModeChange)
           ])
         ),
         new DrawerDivider(),
@@ -146,23 +140,26 @@
   }
 
   void _handleShowSettings() {
-    navigator.pop();
-    navigator.pushNamed('/settings');
+    config.navigator.pop();
+    config.navigator.pushNamed('/settings');
   }
 
   Widget buildToolBar() {
     return new ToolBar(
         left: new IconButton(
           icon: "navigation/menu",
-          onPressed: _handleOpenDrawer),
+          onPressed: _handleOpenDrawer
+        ),
         center: new Text('Stocks'),
         right: [
           new IconButton(
             icon: "action/search",
-            onPressed: _handleSearchBegin),
+            onPressed: _handleSearchBegin
+          ),
           new IconButton(
             icon: "navigation/more_vert",
-            onPressed: _handleMenuShow)
+            onPressed: _handleMenuShow
+          )
         ]
       );
   }
@@ -181,12 +178,12 @@
     return stocks.where((stock) => stock.symbol.contains(regexp));
   }
 
-  Widget buildMarketStockList() {
-    return new Stocklist(stocks: _filterBySearchQuery(stocks).toList());
+  Widget buildMarketStockList(BuildContext context) {
+    return new Stocklist(stocks: _filterBySearchQuery(config.stocks).toList());
   }
 
-  Widget buildPortfolioStocklist() {
-    return new Stocklist(stocks: _filterBySearchQuery(_filterByPortfolio(stocks)).toList());
+  Widget buildPortfolioStocklist(BuildContext context) {
+    return new Stocklist(stocks: _filterBySearchQuery(_filterByPortfolio(config.stocks)).toList());
   }
 
   Widget buildTabNavigator() {
@@ -216,7 +213,7 @@
     return new ToolBar(
       left: new IconButton(
         icon: "navigation/arrow_back",
-        color: Theme.of(this).accentColor,
+        color: Theme.of(context).accentColor,
         onPressed: _handleSearchEnd
       ),
       center: new Input(
@@ -224,7 +221,7 @@
         placeholder: 'Search stocks',
         onChanged: _handleSearchQueryChanged
       ),
-      backgroundColor: Theme.of(this).canvasColor
+      backgroundColor: Theme.of(context).canvasColor
     );
   }
 
@@ -255,17 +252,14 @@
   }
 
   Widget buildFloatingActionButton() {
-    return new TransitionProxy(
-      transitionKey: snackBarKey,
-      child: new FloatingActionButton(
-        child: new Icon(type: 'content/add', size: 24),
-        backgroundColor: Colors.redAccent[200],
-        onPressed: _handleStockPurchased
-      )
+    return new FloatingActionButton(
+      child: new Icon(type: 'content/add', size: 24),
+      backgroundColor: Colors.redAccent[200],
+      onPressed: _handleStockPurchased
     );
   }
 
-  Widget build() {
+  Widget build(BuildContext context) {
     return new Scaffold(
       toolbar: _isSearching ? buildSearchBar() : buildToolBar(),
       body: buildTabNavigator(),
diff --git a/examples/stocks/lib/stock_list.dart b/examples/stocks/lib/stock_list.dart
index 42ea384..a65d735 100644
--- a/examples/stocks/lib/stock_list.dart
+++ b/examples/stocks/lib/stock_list.dart
@@ -4,18 +4,18 @@
 
 part of stocks;
 
-class Stocklist extends Component {
+class Stocklist extends StatelessComponent {
   Stocklist({ Key key, this.stocks }) : super(key: key);
 
   final List<Stock> stocks;
 
-  Widget build() {
+  Widget build(BuildContext context) {
     return new Material(
       type: MaterialType.canvas,
       child: new ScrollableList<Stock>(
         items: stocks,
         itemExtent: StockRow.kHeight,
-        itemBuilder: (Stock stock) => new StockRow(stock: stock)
+        itemBuilder: (BuildContext context, Stock stock) => new StockRow(stock: stock)
       )
     );
   }
diff --git a/examples/stocks/lib/stock_menu.dart b/examples/stocks/lib/stock_menu.dart
index a61073a..f9c3278 100644
--- a/examples/stocks/lib/stock_menu.dart
+++ b/examples/stocks/lib/stock_menu.dart
@@ -4,19 +4,27 @@
 
 part of stocks;
 
-Future showStockMenu(Navigator navigator, { bool autorefresh, ValueChanged onAutorefreshChanged }) {
-  return showMenu(
+enum _MenuItems { add, remove, autorefresh }
+
+Future showStockMenu(NavigatorState navigator, { bool autorefresh, ValueChanged onAutorefreshChanged }) async {
+  switch (await showMenu(
     navigator: navigator,
     position: new MenuPosition(
       right: sky.view.paddingRight,
       top: sky.view.paddingTop
     ),
-    builder: (Navigator navigator) {
+    builder: (NavigatorState navigator) {
       return <PopupMenuItem>[
-        new PopupMenuItem(child: new Text('Add stock')),
-        new PopupMenuItem(child: new Text('Remove stock')),
         new PopupMenuItem(
-          onPressed: () => onAutorefreshChanged(!autorefresh),
+          value: _MenuItems.add,
+          child: new Text('Add stock')
+        ),
+        new PopupMenuItem(
+          value: _MenuItems.remove,
+          child: new Text('Remove stock')
+        ),
+        new PopupMenuItem(
+          value: _MenuItems.autorefresh,
           child: new Row([
               new Flexible(child: new Text('Autorefresh')),
               new Checkbox(
@@ -28,5 +36,28 @@
         ),
       ];
     }
-  );
+  )) {
+    case _MenuItems.autorefresh:
+      onAutorefreshChanged(!autorefresh);
+      break;
+    case _MenuItems.add:
+    case _MenuItems.remove:
+      await showDialog(navigator, (NavigatorState navigator) {
+        return new Dialog(
+          title: new Text('Not Implemented'),
+          content: new Text('This feature has not yet been implemented.'),
+          actions: [
+            new FlatButton(
+              child: new Text('OH WELL'),
+              onPressed: () {
+                navigator.pop(false);
+              }
+            ),
+          ]
+        );
+      });
+      break;
+    default:
+      // menu was canceled.
+  }
 }
\ No newline at end of file
diff --git a/examples/stocks/lib/stock_row.dart b/examples/stocks/lib/stock_row.dart
index f2f4e68..4ebe859 100644
--- a/examples/stocks/lib/stock_row.dart
+++ b/examples/stocks/lib/stock_row.dart
@@ -4,15 +4,14 @@
 
 part of stocks;
 
-class StockRow extends Component {
-
+class StockRow extends StatelessComponent {
   StockRow({ Stock stock }) : this.stock = stock, super(key: new Key(stock.symbol));
 
   final Stock stock;
 
   static const double kHeight = 79.0;
 
-  Widget build() {
+  Widget build(BuildContext context) {
     String lastSale = "\$${stock.lastSale.toStringAsFixed(2)}";
 
     String changeInPrice = "${stock.percentChange.toStringAsFixed(2)}%";
@@ -32,7 +31,7 @@
       new Flexible(
         child: new Text(
           changeInPrice,
-          style: Theme.of(this).text.caption.copyWith(textAlign: TextAlign.right)
+          style: Theme.of(context).text.caption.copyWith(textAlign: TextAlign.right)
         )
       )
     ];
@@ -43,7 +42,7 @@
       height: kHeight,
       decoration: new BoxDecoration(
         border: new Border(
-          bottom: new BorderSide(color: Theme.of(this).dividerColor)
+          bottom: new BorderSide(color: Theme.of(context).dividerColor)
         )
       ),
       child: new Row([
@@ -55,7 +54,7 @@
           child: new Row(
             children,
             alignItems: FlexAlignItems.baseline,
-            textBaseline: DefaultTextStyle.of(this).textBaseline
+            textBaseline: DefaultTextStyle.of(context).textBaseline
           )
         )
       ])
diff --git a/examples/stocks/lib/stock_settings.dart b/examples/stocks/lib/stock_settings.dart
index 19f3125..269afe1 100644
--- a/examples/stocks/lib/stock_settings.dart
+++ b/examples/stocks/lib/stock_settings.dart
@@ -10,42 +10,32 @@
 });
 
 class StockSettings extends StatefulComponent {
+  const StockSettings(this.navigator, this.optimism, this.backup, this.updater);
 
-  StockSettings(this.navigator, this.optimism, this.backup, this.updater);
+  final NavigatorState navigator;
+  final StockMode optimism;
+  final BackupMode backup;
+  final SettingsUpdater updater;
 
-  Navigator navigator;
-  StockMode optimism;
-  BackupMode backup;
-  SettingsUpdater updater;
+  StockSettingsState createState() => new StockSettingsState();
+}
 
-  void syncConstructorArguments(StockSettings source) {
-    navigator = source.navigator;
-    optimism = source.optimism;
-    backup = source.backup;
-    updater = source.updater;
-  }
-
+class StockSettingsState extends State<StockSettings> {
   void _handleOptimismChanged(bool value) {
-    setState(() {
-      optimism = value ? StockMode.optimistic : StockMode.pessimistic;
-    });
-    sendUpdates();
+    sendUpdates(value ? StockMode.optimistic : StockMode.pessimistic, config.backup);
   }
 
   void _handleBackupChanged(bool value) {
-    setState(() {
-      backup = value ? BackupMode.enabled : BackupMode.disabled;
-    });
-    sendUpdates();
+    sendUpdates(config.optimism, value ? BackupMode.enabled : BackupMode.disabled);
   }
 
   void _confirmOptimismChange() {
-    switch (optimism) {
+    switch (config.optimism) {
       case StockMode.optimistic:
         _handleOptimismChanged(false);
         break;
       case StockMode.pessimistic:
-        showDialog(navigator, (navigator) {
+        showDialog(config.navigator, (NavigatorState navigator) {
           return new Dialog(
             title: new Text("Change mode?"),
             content: new Text("Optimistic mode means everything is awesome. Are you sure you can handle that?"),
@@ -72,24 +62,25 @@
     }
   }
 
-  void sendUpdates() {
-    if (updater != null)
-      updater(
+  void sendUpdates(StockMode optimism, BackupMode backup) {
+    if (config.updater != null)
+      config.updater(
         optimism: optimism,
         backup: backup
       );
   }
 
-  Widget buildToolBar() {
+  Widget buildToolBar(BuildContext context) {
     return new ToolBar(
       left: new IconButton(
         icon: 'navigation/arrow_back',
-        onPressed: navigator.pop),
+        onPressed: config.navigator.pop
+      ),
       center: new Text('Settings')
     );
   }
 
-  Widget buildSettingsPane() {
+  Widget buildSettingsPane(BuildContext context) {
     // TODO(ianh): Once we have the gesture API hooked up, fix https://github.com/domokit/mojo/issues/281
     // (whereby tapping the widgets below causes both the widget and the menu item to fire their callbacks)
     return new Material(
@@ -103,15 +94,21 @@
               onPressed: () => _confirmOptimismChange(),
               child: new Row([
                 new Flexible(child: new Text('Everything is awesome')),
-                new Checkbox(value: optimism == StockMode.optimistic, onChanged: (_) => _confirmOptimismChange()),
+                new Checkbox(
+                  value: config.optimism == StockMode.optimistic,
+                  onChanged: (_) => _confirmOptimismChange()
+                ),
               ])
             ),
             new DrawerItem(
               icon: 'action/backup',
-              onPressed: () { _handleBackupChanged(!(backup == BackupMode.enabled)); },
+              onPressed: () { _handleBackupChanged(!(config.backup == BackupMode.enabled)); },
               child: new Row([
                 new Flexible(child: new Text('Back up stock list to the cloud')),
-                new Switch(value: backup == BackupMode.enabled, onChanged: _handleBackupChanged),
+                new Switch(
+                  value: config.backup == BackupMode.enabled,
+                  onChanged: _handleBackupChanged
+                ),
               ])
             ),
           ])
@@ -120,10 +117,10 @@
     );
   }
 
-  Widget build() {
+  Widget build(BuildContext context) {
     return new Scaffold(
-      toolbar: buildToolBar(),
-      body: buildSettingsPane()
+      toolbar: buildToolBar(context),
+      body: buildSettingsPane(context)
     );
   }
 }
diff --git a/packages/flutter/lib/src/fn3.dart b/packages/flutter/lib/src/fn3.dart
index 5f49c09..e6a93bb 100644
--- a/packages/flutter/lib/src/fn3.dart
+++ b/packages/flutter/lib/src/fn3.dart
@@ -5,6 +5,7 @@
 library fn3;
 
 export 'fn3/animated_component.dart';
+export 'fn3/app.dart';
 export 'fn3/basic.dart';
 export 'fn3/binding.dart';
 export 'fn3/button_state.dart';
diff --git a/packages/flutter/lib/src/fn3/app.dart b/packages/flutter/lib/src/fn3/app.dart
new file mode 100644
index 0000000..201af6e
--- /dev/null
+++ b/packages/flutter/lib/src/fn3/app.dart
@@ -0,0 +1,83 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:sky' as sky;
+
+import 'package:sky/material.dart';
+import 'package:sky/painting.dart';
+import 'package:sky/src/fn3/basic.dart';
+import 'package:sky/src/fn3/binding.dart';
+import 'package:sky/src/fn3/framework.dart';
+import 'package:sky/src/fn3/navigator.dart';
+import 'package:sky/src/fn3/theme.dart';
+import 'package:sky/src/fn3/title.dart';
+
+const TextStyle _errorTextStyle = const TextStyle(
+  color: const Color(0xD0FF0000),
+  fontFamily: 'monospace',
+  fontSize: 48.0,
+  fontWeight: FontWeight.w900,
+  textAlign: TextAlign.right,
+  decoration: underline,
+  decorationColor: const Color(0xFFFF00),
+  decorationStyle: TextDecorationStyle.double
+);
+
+class App extends StatefulComponent {
+  App({
+    Key key,
+    this.title,
+    this.theme,
+    this.routes
+  }): super(key: key);
+
+  final String title;
+  final ThemeData theme;
+  final Map<String, RouteBuilder> routes;
+
+  AppState createState() => new AppState();
+}
+
+class AppState extends State<App> {
+
+  GlobalObjectKey _navigator;
+
+  void initState(BuildContext context) {
+    super.initState(context);
+    _navigator = new GlobalObjectKey(this);
+    WidgetFlutterBinding.instance.addEventListener(_backHandler);
+  }
+
+  void dispose() {
+    WidgetFlutterBinding.instance.removeEventListener(_backHandler);
+    super.dispose();
+  }
+
+  void _backHandler(sky.Event event) {
+    assert(mounted);
+    if (event.type == 'back') {
+      NavigatorState navigator = _navigator.currentState;
+      assert(navigator != null);
+      if (navigator.hasPreviousRoute)
+        navigator.pop();
+    }
+  }
+
+  Widget build(BuildContext context) {
+    return new Theme(
+      data: config.theme,
+      child: new DefaultTextStyle(
+        style: _errorTextStyle,
+        child: new Title(
+          title: config.title,
+          child: new Navigator(
+            key: _navigator,
+            routes: config.routes
+          )
+        )
+      )
+    );
+  }
+
+}
\ No newline at end of file
diff --git a/packages/flutter/lib/src/fn3/checkbox.dart b/packages/flutter/lib/src/fn3/checkbox.dart
index 0b138c6..a22146f 100644
--- a/packages/flutter/lib/src/fn3/checkbox.dart
+++ b/packages/flutter/lib/src/fn3/checkbox.dart
@@ -44,10 +44,11 @@
         ? _kLightUncheckedColor
         : _kDarkUncheckedColor;
     return new _CheckboxWrapper(
-        value: value,
-        onChanged: onChanged,
-        uncheckedColor: uncheckedColor,
-        accentColor: themeData.accentColor);
+      value: value,
+      onChanged: onChanged,
+      uncheckedColor: uncheckedColor,
+      accentColor: themeData.accentColor
+    );
   }
 }
 
@@ -55,9 +56,16 @@
 // order to get an accent color from a Theme but Components do not know how to
 // host RenderObjects.
 class _CheckboxWrapper extends LeafRenderObjectWidget {
-  _CheckboxWrapper({Key key, this.value, this.onChanged, this.uncheckedColor,
-      this.accentColor})
-      : super(key: key);
+  _CheckboxWrapper({
+    Key key,
+    this.value,
+    this.onChanged,
+    this.uncheckedColor,
+    this.accentColor
+  }): super(key: key) {
+    assert(uncheckedColor != null);
+    assert(accentColor != null);
+  }
 
   final bool value;
   final ValueChanged onChanged;
@@ -65,7 +73,11 @@
   final Color accentColor;
 
   _RenderCheckbox createRenderObject() => new _RenderCheckbox(
-      value: value, uncheckedColor: uncheckedColor, onChanged: onChanged);
+    value: value,
+    accentColor: accentColor,
+    uncheckedColor: uncheckedColor,
+    onChanged: onChanged
+  );
 
   void updateRenderObject(_RenderCheckbox renderObject, _CheckboxWrapper oldWidget) {
     renderObject.value = value;
@@ -76,25 +88,38 @@
 }
 
 class _RenderCheckbox extends RenderToggleable {
-  _RenderCheckbox({bool value, Color uncheckedColor, ValueChanged onChanged})
-      : _uncheckedColor = uncheckedColor,
-        super(
-            value: value,
-            onChanged: onChanged,
-            size: new Size(_kEdgeSize, _kEdgeSize)) {}
+  _RenderCheckbox({
+    bool value,
+    Color uncheckedColor,
+    Color accentColor,
+    ValueChanged onChanged
+  }): _uncheckedColor = uncheckedColor,
+      _accentColor = accentColor,
+      super(
+        value: value,
+        onChanged: onChanged,
+        size: new Size(_kEdgeSize, _kEdgeSize)
+      ) {
+    assert(uncheckedColor != null);
+    assert(accentColor != null);
+  }
 
   Color _uncheckedColor;
   Color get uncheckedColor => _uncheckedColor;
 
   void set uncheckedColor(Color value) {
-    if (value == _uncheckedColor) return;
+    assert(value != null);
+    if (value == _uncheckedColor)
+      return;
     _uncheckedColor = value;
     markNeedsPaint();
   }
 
   Color _accentColor;
   void set accentColor(Color value) {
-    if (value == _accentColor) return;
+    assert(value != null);
+    if (value == _accentColor)
+      return;
     _accentColor = value;
     markNeedsPaint();
   }
diff --git a/packages/flutter/lib/src/fn3/dialog.dart b/packages/flutter/lib/src/fn3/dialog.dart
index 5f5438b..02aef0d 100644
--- a/packages/flutter/lib/src/fn3/dialog.dart
+++ b/packages/flutter/lib/src/fn3/dialog.dart
@@ -16,7 +16,7 @@
 import 'package:sky/src/fn3/theme.dart';
 import 'package:sky/src/fn3/transitions.dart';
 
-typedef Widget DialogBuilder(Navigator navigator);
+typedef Dialog DialogBuilder(NavigatorState navigator);
 
 /// A material design dialog
 ///
@@ -132,7 +132,7 @@
 
 const Duration _kTransitionDuration = const Duration(milliseconds: 150);
 
-class DialogRoute extends RouteBase {
+class DialogRoute extends Route {
   DialogRoute({ this.completer, this.builder });
 
   final Completer completer;
diff --git a/packages/flutter/lib/src/fn3/navigator.dart b/packages/flutter/lib/src/fn3/navigator.dart
index 834e0da..b9e7b37 100644
--- a/packages/flutter/lib/src/fn3/navigator.dart
+++ b/packages/flutter/lib/src/fn3/navigator.dart
@@ -8,11 +8,11 @@
 import 'package:sky/src/fn3/framework.dart';
 import 'package:sky/src/fn3/transitions.dart';
 
-typedef Widget RouteBuilder(NavigatorState navigator, RouteBase route);
+typedef Widget RouteBuilder(NavigatorState navigator, Route route);
 
 typedef void NotificationCallback();
 
-abstract class RouteBase {
+abstract class Route {
   AnimationPerformance _performance;
   NotificationCallback onDismissed;
   NotificationCallback onCompleted;
@@ -57,10 +57,10 @@
 
 const Duration _kTransitionDuration = const Duration(milliseconds: 150);
 const Point _kTransitionStartPoint = const Point(0.0, 75.0);
-class Route extends RouteBase {
-  Route({ this.name, this.builder });
 
-  final String name;
+class PageRoute extends Route {
+  PageRoute(this.builder);
+
   final RouteBuilder builder;
 
   bool get isOpaque => true;
@@ -81,16 +81,16 @@
       )
     );
   }
-
-  String toString() => '$runtimeType(name="$name")';
 }
 
-class RouteState extends RouteBase {
-  RouteState({ this.callback, this.route, this.owner });
+typedef void RouteStateCallback(RouteState route);
 
-  Function callback;
-  RouteBase route;
+class RouteState extends Route {
+  RouteState({ this.route, this.owner, this.callback });
+
+  Route route;
   State owner;
+  RouteStateCallback callback;
 
   bool get isOpaque => false;
 
@@ -105,99 +105,81 @@
   Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) => null;
 }
 
-class NavigatorHistory {
-
-  NavigatorHistory(List<Route> routes) {
-    for (Route route in routes) {
-      if (route.name != null)
-        namedRoutes[route.name] = route;
-    }
-    recents.add(routes[0]);
-  }
-
-  List<RouteBase> recents = new List<RouteBase>();
-  int index = 0;
-  Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();
-
-  RouteBase get currentRoute => recents[index];
-  bool hasPrevious() => index > 0;
-
-  void pushNamed(String name) {
-    Route route = namedRoutes[name];
-    assert(route != null);
-    push(route);
-  }
-
-  void push(RouteBase route) {
-    assert(!_debugCurrentlyHaveRoute(route));
-    recents.insert(index + 1, route);
-    index++;
-  }
-
-  void pop([dynamic result]) {
-    if (index > 0) {
-      RouteBase route = recents[index];
-      route.popState(result);
-      index--;
-    }
-  }
-
-  bool _debugCurrentlyHaveRoute(RouteBase route) {
-    return recents.any((candidate) => candidate == route);
-  }
-}
-
 class Navigator extends StatefulComponent {
-  Navigator(this.history, { Key key }) : super(key: key);
+  Navigator({ this.routes, Key key }) : super(key: key) {
+    // To use a navigator, you must at a minimum define the route with the name '/'.
+    assert(routes.containsKey('/'));
+  }
 
-  final NavigatorHistory history;
+  final Map<String, RouteBuilder> routes;
 
   NavigatorState createState() => new NavigatorState();
 }
 
 class NavigatorState extends State<Navigator> {
-  RouteBase get currentRoute => config.history.currentRoute;
+
+  List<Route> _history = new List<Route>();
+  int _currentPosition = 0;
+
+  Route get currentRoute => _history[_currentPosition];
+  bool get hasPreviousRoute => _history.length > 1;
+
+  void initState(BuildContext context) {
+    super.initState(context);
+    PageRoute route = new PageRoute(config.routes['/']);
+    assert(route != null);
+    _history.add(route);
+  }
 
   void pushState(State owner, Function callback) {
-    RouteBase route = new RouteState(
+    push(new RouteState(
+      route: currentRoute,
       owner: owner,
-      callback: callback,
-      route: currentRoute
-    );
-    push(route);
+      callback: callback
+    ));
   }
 
   void pushNamed(String name) {
-    setState(() {
-      config.history.pushNamed(name);
-    });
+    PageRoute route = new PageRoute(config.routes[name]);
+    assert(route != null);
+    push(route);
   }
 
-  void push(RouteBase route) {
+  void push(Route route) {
+    assert(!_debugCurrentlyHaveRoute(route));
+    _history.insert(_currentPosition + 1, route);
     setState(() {
-      config.history.push(route);
+      _currentPosition += 1;
     });
   }
 
   void pop([dynamic result]) {
-    setState(() {
-      config.history.pop(result);
-    });
+    if (_currentPosition > 0) {
+      Route route = _history[_currentPosition];
+      route.popState(result);
+      setState(() {
+        _currentPosition -= 1;
+      });
+    }
+  }
+
+  bool _debugCurrentlyHaveRoute(Route route) {
+    return _history.any((candidate) => candidate == route);
   }
 
   Widget build(BuildContext context) {
     List<Widget> visibleRoutes = new List<Widget>();
-    for (int i = config.history.recents.length-1; i >= 0; i -= 1) {
-      RouteBase route = config.history.recents[i];
+    for (int i = _history.length-1; i >= 0; i -= 1) {
+      Route route = _history[i];
       if (!route.hasContent)
         continue;
       WatchableAnimationPerformance performance = route.ensurePerformance(
-        direction: (i <= config.history.index) ? Direction.forward : Direction.reverse
+        direction: (i <= _currentPosition) ? Direction.forward : Direction.reverse
       );
       route.onDismissed = () {
         setState(() {
-          assert(config.history.recents.contains(route));
-          config.history.recents.remove(route);
+          assert(_history.contains(route));
+          _history.remove(route);
         });
       };
       Key key = new ObjectKey(route);
diff --git a/packages/flutter/lib/src/fn3/popup_menu.dart b/packages/flutter/lib/src/fn3/popup_menu.dart
index c34d9b2..b748cf5 100644
--- a/packages/flutter/lib/src/fn3/popup_menu.dart
+++ b/packages/flutter/lib/src/fn3/popup_menu.dart
@@ -6,8 +6,8 @@
 import 'dart:sky' as sky;
 
 import 'package:sky/animation.dart';
-import 'package:sky/painting.dart';
 import 'package:sky/material.dart';
+import 'package:sky/painting.dart';
 import 'package:sky/src/fn3/basic.dart';
 import 'package:sky/src/fn3/focus.dart';
 import 'package:sky/src/fn3/framework.dart';
@@ -15,6 +15,7 @@
 import 'package:sky/src/fn3/navigator.dart';
 import 'package:sky/src/fn3/popup_menu_item.dart';
 import 'package:sky/src/fn3/scrollable.dart';
+import 'package:sky/src/fn3/theme.dart';
 import 'package:sky/src/fn3/transitions.dart';
 
 const Duration _kMenuDuration = const Duration(milliseconds: 300);
@@ -26,6 +27,8 @@
 const double _kMenuHorizontalPadding = 16.0;
 const double _kMenuVerticalPadding = 8.0;
 
+typedef List<PopupMenuItem> PopupMenuItemsBuilder(NavigatorState navigator);
+
 class PopupMenu extends StatefulComponent {
   PopupMenu({
     Key key,
@@ -49,25 +52,10 @@
 class PopupMenuState extends State<PopupMenu> {
   void initState(BuildContext context) {
     super.initState(context);
-    _updateBoxPainter();
     config.performance.addListener(_performanceChanged);
   }
 
-  BoxPainter _painter;
-
-  void _updateBoxPainter() {
-    _painter = new BoxPainter(
-      new BoxDecoration(
-        backgroundColor: Colors.grey[50],
-        borderRadius: 2.0,
-        boxShadow: shadows[config.level]
-      )
-    );
-  }
-
   void didUpdateConfig(PopupMenu oldConfig) {
-    if (config.level != config.level)
-      _updateBoxPainter();
     if (config.performance != oldConfig.performance) {
       oldConfig.performance.removeListener(_performanceChanged);
       config.performance.addListener(_performanceChanged);
@@ -85,7 +73,19 @@
     });
   }
 
+  BoxPainter _painter;
+
+  void _updateBoxPainter(BoxDecoration decoration) {
+    if (_painter == null || _painter.decoration != decoration)
+      _painter = new BoxPainter(decoration);
+  }
+
   Widget build(BuildContext context) {
+    _updateBoxPainter(new BoxDecoration(
+      backgroundColor: Theme.of(context).canvasColor,
+      borderRadius: 2.0,
+      boxShadow: shadows[config.level]
+    ));
     double unit = 1.0 / (config.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
     List<Widget> children = [];
     for (int i = 0; i < config.items.length; ++i) {
@@ -151,7 +151,7 @@
   final double left;
 }
 
-class MenuRoute extends RouteBase {
+class MenuRoute extends Route {
   MenuRoute({ this.completer, this.position, this.builder, this.level });
 
   final Completer completer;
@@ -194,8 +194,6 @@
   }
 }
 
-typedef List<PopupMenuItem> PopupMenuItemsBuilder(NavigatorState navigator);
-
 Future showMenu({ NavigatorState navigator, MenuPosition position, PopupMenuItemsBuilder builder, int level: 4 }) {
   Completer completer = new Completer();
   navigator.push(new MenuRoute(
diff --git a/packages/flutter/lib/src/material/typography.dart b/packages/flutter/lib/src/material/typography.dart
index 8be679e..37a2626 100644
--- a/packages/flutter/lib/src/material/typography.dart
+++ b/packages/flutter/lib/src/material/typography.dart
@@ -63,6 +63,7 @@
   // TODO(abarth): Maybe this should be hard-coded in Scaffold?
   static const String typeface = 'font-family: sans-serif';
 
+  // TODO(ianh): Remove this when we remove fn2, now that it's hard-coded in App.
   static const TextStyle error = const TextStyle(
     color: const Color(0xD0FF0000),
     fontFamily: 'monospace',
diff --git a/packages/flutter/lib/src/rendering/sky_binding.dart b/packages/flutter/lib/src/rendering/sky_binding.dart
index 02a89bd..bab387b 100644
--- a/packages/flutter/lib/src/rendering/sky_binding.dart
+++ b/packages/flutter/lib/src/rendering/sky_binding.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// TODO(ianh): rename this file 'binding.dart'
+
 import 'dart:sky' as sky;
 
 import 'package:sky/animation.dart';
@@ -39,6 +41,7 @@
 }
 
 /// The glue between the render tree and the sky engine
+// TODO(ianh): rename this class FlutterBinding
 class SkyBinding extends HitTestTarget {
 
   SkyBinding({ RenderBox root: null, RenderView renderViewOverride }) {
diff --git a/packages/unit/test/widget/navigator_test.dart b/packages/unit/test/widget/navigator_test.dart
index 7532178..7b6ab89 100644
--- a/packages/unit/test/widget/navigator_test.dart
+++ b/packages/unit/test/widget/navigator_test.dart
@@ -50,18 +50,12 @@
   test('Can navigator navigate to and from a stateful component', () {
     WidgetTester tester = new WidgetTester();
 
-    final NavigatorHistory routes = new NavigatorHistory([
-      new Route(
-        name: '/',
-        builder: (navigator, route) => new FirstComponent(navigator)
-      ),
-      new Route(
-        name: '/second',
-        builder: (navigator, route) => new SecondComponent(navigator)
-      )
-    ]);
+    final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
+      '/': (navigator, route) => new FirstComponent(navigator),
+      '/second': (navigator, route) => new SecondComponent(navigator),
+    };
 
-    tester.pumpFrame(new Navigator(routes));
+    tester.pumpFrame(new Navigator(routes: routes));
 
     expect(tester.findText('X'), isNotNull);
     expect(tester.findText('Y'), isNull);