Commit f2b7dd62 authored by Hixie's avatar Hixie

Dynamic named routes

Make it possible for named routes to be generated on the fly.

To demonstrate this, you can now long-press a stock to open it.

Next steps:

 - transitions between (named) states that follow full material logic,
   e.g. in the case of the stock row to stock page transition, expanding
   the row into a raised sheet of material and expanding it to fit the
   screen, leaving the toolbar in place but cross-fading the old
   contents to the new contents.

 - more information in the stock view.

While I was here I also made Material have an opinion about default text
style, so if you forget to set one, it just uses body1.

Also, fixed bugs introduced recently that made RouteState and MenuRoute
not work properly.
parent 1203b08b
......@@ -22,6 +22,7 @@ part 'stock_list.dart';
part 'stock_menu.dart';
part 'stock_row.dart';
part 'stock_settings.dart';
part 'stock_symbol_viewer.dart';
part 'stock_types.dart';
class StocksApp extends StatefulComponent {
......@@ -30,13 +31,14 @@ class StocksApp extends StatefulComponent {
class StocksAppState extends State<StocksApp> {
final List<Stock> _stocks = [];
final Map<String, Stock> _stocks = <String, Stock>{};
final List<String> _symbols = <String>[];
void initState(BuildContext context) {
super.initState(context);
new StockDataFetcher((StockData data) {
setState(() {
data.appendTo(_stocks);
data.appendTo(_stocks, _symbols);
});
});
}
......@@ -72,14 +74,29 @@ class StocksAppState extends State<StocksApp> {
}
}
RouteBuilder _getRoute(String name) {
List<String> path = name.split('/');
if (path[0] != '')
return null;
if (path[1] == 'stock') {
if (path.length != 3)
return null;
if (_stocks.containsKey(path[2]))
return (navigator, route) => new StockSymbolViewer(navigator, _stocks[path[2]]);
return null;
}
return null;
}
Widget build(BuildContext context) {
return new App(
title: 'Stocks',
theme: theme,
routes: <String, RouteBuilder>{
'/': (navigator, route) => new StockHome(navigator, _stocks, _optimismSetting, modeUpdater),
'/': (navigator, route) => new StockHome(navigator, _stocks, _symbols, _optimismSetting, modeUpdater),
'/settings': (navigator, route) => new StockSettings(navigator, _optimismSetting, _backupSetting, settingsUpdater)
}
},
onGenerateRoute: _getRoute
);
}
}
......
......@@ -42,9 +42,13 @@ class StockData {
StockData(this._data);
void appendTo(List<Stock> stocks) {
for (List<String> fields in _data)
stocks.add(new Stock.fromFields(fields));
void appendTo(Map<String, Stock> stocks, List<String> symbols) {
for (List<String> fields in _data) {
Stock stock = new Stock.fromFields(fields);
symbols.add(stock.symbol);
stocks[stock.symbol] = stock;
}
symbols.sort();
}
}
......
......@@ -9,10 +9,11 @@ typedef void ModeUpdater(StockMode mode);
const Duration _kSnackbarSlideDuration = const Duration(milliseconds: 200);
class StockHome extends StatefulComponent {
StockHome(this.navigator, this.stocks, this.stockMode, this.modeUpdater);
StockHome(this.navigator, this.stocks, this.symbols, this.stockMode, this.modeUpdater);
final NavigatorState navigator;
final List<Stock> stocks;
final Map<String, Stock> stocks;
final List<String> symbols;
final StockMode stockMode;
final ModeUpdater modeUpdater;
......@@ -170,10 +171,9 @@ class StockHomeState extends State<StockHome> {
}
int selectedTabIndex = 0;
List<String> portfolioSymbols = ["AAPL","FIZZ", "FIVE", "FLAT", "ZINC", "ZNGA"];
Iterable<Stock> _filterByPortfolio(Iterable<Stock> stocks) {
return stocks.where((stock) => portfolioSymbols.contains(stock.symbol));
Iterable<Stock> _getStockList(Iterable<String> symbols) {
return symbols.map((symbol) => config.stocks[symbol]);
}
Iterable<Stock> _filterBySearchQuery(Iterable<Stock> stocks) {
......@@ -191,20 +191,25 @@ class StockHomeState extends State<StockHome> {
stock.percentChange = 100.0 * (1.0 / stock.lastSale);
stock.lastSale += 1.0;
});
},
onOpen: (Stock stock) {
config.navigator.pushNamed('/stock/${stock.symbol}');
}
);
}
static const List<String> portfolioSymbols = const <String>["AAPL","FIZZ", "FIVE", "FLAT", "ZINC", "ZNGA"];
Widget buildTabNavigator() {
return new TabNavigator(
views: <TabNavigatorView>[
new TabNavigatorView(
label: const TabLabel(text: 'MARKET'),
builder: (BuildContext context) => buildStockList(context, _filterBySearchQuery(config.stocks))
builder: (BuildContext context) => buildStockList(context, _filterBySearchQuery(_getStockList(config.symbols)).toList())
),
new TabNavigatorView(
label: const TabLabel(text: 'PORTFOLIO'),
builder: (BuildContext context) => buildStockList(context, _filterByPortfolio(config.stocks))
builder: (BuildContext context) => buildStockList(context, _filterBySearchQuery(_getStockList(portfolioSymbols)).toList())
)
],
selectedIndex: selectedTabIndex,
......
......@@ -7,10 +7,11 @@ part of stocks;
typedef void StockActionListener(Stock stock);
class StockList extends StatelessComponent {
StockList({ Key key, this.stocks, this.onAction }) : super(key: key);
StockList({ Key key, this.stocks, this.onAction, this.onOpen }) : super(key: key);
final List<Stock> stocks;
final StockActionListener onAction;
final StockActionListener onOpen;
Widget build(BuildContext context) {
return new Material(
......@@ -21,7 +22,8 @@ class StockList extends StatelessComponent {
itemBuilder: (BuildContext context, Stock stock) {
return new StockRow(
stock: stock,
onPressed: () { onAction(stock); }
onPressed: () { onAction(stock); },
onLongPressed: () { onOpen(stock); }
);
}
)
......
......@@ -5,10 +5,15 @@
part of stocks;
class StockRow extends StatelessComponent {
StockRow({ Stock stock, this.onPressed }) : this.stock = stock, super(key: new Key(stock.symbol));
StockRow({
Stock stock,
this.onPressed,
this.onLongPressed
}) : this.stock = stock, super(key: new Key(stock.symbol));
final Stock stock;
final GestureTapListener onPressed;
final GestureLongPressListener onLongPressed;
static const double kHeight = 79.0;
......@@ -39,6 +44,7 @@ class StockRow extends StatelessComponent {
return new GestureDetector(
onTap: onPressed,
onLongPress: onLongPressed,
child: new InkWell(
child: new Container(
padding: const EdgeDims(16.0, 16.0, 20.0, 16.0),
......
// 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.
part of stocks;
class StockSymbolViewer extends StatefulComponent {
StockSymbolViewer(this.navigator, this.stock);
final NavigatorState navigator;
final Stock stock;
StockSymbolViewerState createState() => new StockSymbolViewerState();
}
class StockSymbolViewerState extends State<StockSymbolViewer> {
Widget build(BuildContext context) {
String lastSale = "\$${config.stock.lastSale.toStringAsFixed(2)}";
String changeInPrice = "${config.stock.percentChange.toStringAsFixed(2)}%";
if (config.stock.percentChange > 0) changeInPrice = "+" + changeInPrice;
TextStyle headings = Theme.of(context).text.body2;
return new Scaffold(
toolbar: new ToolBar(
left: new IconButton(
icon: 'navigation/arrow_back',
onPressed: config.navigator.pop
),
center: new Text('${config.stock.name} (${config.stock.symbol})')
),
body: new Material(
type: MaterialType.canvas,
child: new Block([
new Container(
padding: new EdgeDims.all(20.0),
child: new Column([
new Text('Last Sale', style: headings),
new Text('${lastSale} (${changeInPrice})'),
new Container(
height: 8.0
),
new Text('Market Cap', style: headings),
new Text('${config.stock.marketCap}'),
],
alignItems: FlexAlignItems.stretch
)
)
])
)
);
}
}
......@@ -30,12 +30,14 @@ class App extends StatefulComponent {
Key key,
this.title,
this.theme,
this.routes
this.routes,
this.onGenerateRoute
}): super(key: key);
final String title;
final ThemeData theme;
final Map<String, RouteBuilder> routes;
final RouteGenerator onGenerateRoute;
AppState createState() => new AppState();
}
......@@ -76,7 +78,8 @@ class AppState extends State<App> {
title: config.title,
child: new Navigator(
key: _navigator,
routes: config.routes
routes: config.routes,
onGenerateRoute: config.onGenerateRoute
)
)
)
......
......@@ -62,7 +62,9 @@ class Material extends StatelessComponent {
}
}
// TODO(abarth): This should use AnimatedContainer.
return new Container(
return new DefaultTextStyle(
style: Theme.of(context).text.body1,
child: new Container(
decoration: new BoxDecoration(
backgroundColor: getBackgroundColor(context),
borderRadius: edges[type],
......@@ -70,6 +72,7 @@ class Material extends StatelessComponent {
shape: type == MaterialType.circle ? Shape.circle : Shape.rectangle
),
child: contents
)
);
}
}
......@@ -9,15 +9,24 @@ import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/transitions.dart';
typedef Widget RouteBuilder(NavigatorState navigator, Route route);
typedef RouteBuilder RouteGenerator(String name);
typedef void RouteStateCallback(RouteState route);
typedef void NotificationCallback();
class Navigator extends StatefulComponent {
Navigator({ this.routes, Key key }) : super(key: key) {
Navigator({
Key key,
this.routes,
this.onGenerateRoute, // you need to implement this if you pushNamed() to names that might not be in routes.
this.onUnknownRoute // 404 generator. You only need to implement this if you have a way to navigate to arbitrary names.
}) : super(key: key) {
// To use a navigator, you must at a minimum define the route with the name '/'.
assert(routes.containsKey('/'));
}
final Map<String, RouteBuilder> routes;
final RouteGenerator onGenerateRoute;
final RouteBuilder onUnknownRoute;
NavigatorState createState() => new NavigatorState();
}
......@@ -47,9 +56,17 @@ class NavigatorState extends State<Navigator> {
}
void pushNamed(String name) {
PageRoute route = new PageRoute(config.routes[name]);
assert(route != null);
push(route);
RouteBuilder builder;
if (!config.routes.containsKey(name)) {
assert(config.onGenerateRoute != null);
builder = config.onGenerateRoute(name);
} else {
builder = config.routes[name];
}
if (builder == null)
builder = config.onUnknownRoute; // 404!
assert(builder != null); // 404 getting your 404!
push(new PageRoute(builder));
}
void push(Route route) {
......@@ -172,6 +189,10 @@ abstract class Route {
/// NavigatorState.pushState().
///
/// Set hasContent to false if you have nothing useful to return from build().
///
/// modal must be false if hasContent is false, since otherwise any
/// interaction with the system at all would imply that the current route is
/// popped, which would be pointless.
bool get hasContent => true;
/// If ephemeral is true, then to explicitly pop the route you have to use
......@@ -196,6 +217,7 @@ abstract class Route {
/// popped.
///
/// ephemeral must be true if modal is false.
/// hasContent must be true if modal is true.
bool get modal => true;
/// If opaque is true, then routes below this one will not be built or painted
......@@ -253,8 +275,6 @@ class PageRoute extends Route {
}
}
typedef void RouteStateCallback(RouteState route);
class RouteState extends Route {
RouteState({ this.route, this.owner, this.callback });
......@@ -262,6 +282,8 @@ class RouteState extends Route {
State owner;
RouteStateCallback callback;
bool get hasContent => false;
bool get modal => false;
bool get opaque => false;
void didPop([dynamic result]) {
......@@ -271,6 +293,5 @@ class RouteState extends Route {
super.didPop(result);
}
bool get hasContent => false;
Widget build(Key key, NavigatorState navigator) => null;
}
......@@ -167,7 +167,7 @@ class MenuRoute extends Route {
return result;
}
bool get ephemeral => true;
bool get ephemeral => false; // we could make this true, but then we'd have to use popRoute(), not pop(), in menus
bool get modal => true;
Duration get transitionDuration => _kMenuDuration;
......
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