Commit 8f91be5a authored by Ian Hickson's avatar Ian Hickson

Merge pull request #1405 from Hixie/dynamic-routes

Dynamic named routes
parents 32558478 f2b7dd62
...@@ -22,6 +22,7 @@ part 'stock_list.dart'; ...@@ -22,6 +22,7 @@ part 'stock_list.dart';
part 'stock_menu.dart'; part 'stock_menu.dart';
part 'stock_row.dart'; part 'stock_row.dart';
part 'stock_settings.dart'; part 'stock_settings.dart';
part 'stock_symbol_viewer.dart';
part 'stock_types.dart'; part 'stock_types.dart';
class StocksApp extends StatefulComponent { class StocksApp extends StatefulComponent {
...@@ -30,13 +31,14 @@ class StocksApp extends StatefulComponent { ...@@ -30,13 +31,14 @@ class StocksApp extends StatefulComponent {
class StocksAppState extends State<StocksApp> { 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) { void initState(BuildContext context) {
super.initState(context); super.initState(context);
new StockDataFetcher((StockData data) { new StockDataFetcher((StockData data) {
setState(() { setState(() {
data.appendTo(_stocks); data.appendTo(_stocks, _symbols);
}); });
}); });
} }
...@@ -72,14 +74,29 @@ class StocksAppState extends State<StocksApp> { ...@@ -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) { Widget build(BuildContext context) {
return new App( return new App(
title: 'Stocks', title: 'Stocks',
theme: theme, theme: theme,
routes: <String, RouteBuilder>{ 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) '/settings': (navigator, route) => new StockSettings(navigator, _optimismSetting, _backupSetting, settingsUpdater)
} },
onGenerateRoute: _getRoute
); );
} }
} }
......
...@@ -42,9 +42,13 @@ class StockData { ...@@ -42,9 +42,13 @@ class StockData {
StockData(this._data); StockData(this._data);
void appendTo(List<Stock> stocks) { void appendTo(Map<String, Stock> stocks, List<String> symbols) {
for (List<String> fields in _data) for (List<String> fields in _data) {
stocks.add(new Stock.fromFields(fields)); 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); ...@@ -9,10 +9,11 @@ typedef void ModeUpdater(StockMode mode);
const Duration _kSnackbarSlideDuration = const Duration(milliseconds: 200); const Duration _kSnackbarSlideDuration = const Duration(milliseconds: 200);
class StockHome extends StatefulComponent { 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 NavigatorState navigator;
final List<Stock> stocks; final Map<String, Stock> stocks;
final List<String> symbols;
final StockMode stockMode; final StockMode stockMode;
final ModeUpdater modeUpdater; final ModeUpdater modeUpdater;
...@@ -170,10 +171,9 @@ class StockHomeState extends State<StockHome> { ...@@ -170,10 +171,9 @@ class StockHomeState extends State<StockHome> {
} }
int selectedTabIndex = 0; int selectedTabIndex = 0;
List<String> portfolioSymbols = ["AAPL","FIZZ", "FIVE", "FLAT", "ZINC", "ZNGA"];
Iterable<Stock> _filterByPortfolio(Iterable<Stock> stocks) { Iterable<Stock> _getStockList(Iterable<String> symbols) {
return stocks.where((stock) => portfolioSymbols.contains(stock.symbol)); return symbols.map((symbol) => config.stocks[symbol]);
} }
Iterable<Stock> _filterBySearchQuery(Iterable<Stock> stocks) { Iterable<Stock> _filterBySearchQuery(Iterable<Stock> stocks) {
...@@ -191,20 +191,25 @@ class StockHomeState extends State<StockHome> { ...@@ -191,20 +191,25 @@ class StockHomeState extends State<StockHome> {
stock.percentChange = 100.0 * (1.0 / stock.lastSale); stock.percentChange = 100.0 * (1.0 / stock.lastSale);
stock.lastSale += 1.0; 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() { Widget buildTabNavigator() {
return new TabNavigator( return new TabNavigator(
views: <TabNavigatorView>[ views: <TabNavigatorView>[
new TabNavigatorView( new TabNavigatorView(
label: const TabLabel(text: 'MARKET'), 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( new TabNavigatorView(
label: const TabLabel(text: 'PORTFOLIO'), label: const TabLabel(text: 'PORTFOLIO'),
builder: (BuildContext context) => buildStockList(context, _filterByPortfolio(config.stocks)) builder: (BuildContext context) => buildStockList(context, _filterBySearchQuery(_getStockList(portfolioSymbols)).toList())
) )
], ],
selectedIndex: selectedTabIndex, selectedIndex: selectedTabIndex,
......
...@@ -7,10 +7,11 @@ part of stocks; ...@@ -7,10 +7,11 @@ part of stocks;
typedef void StockActionListener(Stock stock); typedef void StockActionListener(Stock stock);
class StockList extends StatelessComponent { 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 List<Stock> stocks;
final StockActionListener onAction; final StockActionListener onAction;
final StockActionListener onOpen;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Material( return new Material(
...@@ -21,7 +22,8 @@ class StockList extends StatelessComponent { ...@@ -21,7 +22,8 @@ class StockList extends StatelessComponent {
itemBuilder: (BuildContext context, Stock stock) { itemBuilder: (BuildContext context, Stock stock) {
return new StockRow( return new StockRow(
stock: stock, stock: stock,
onPressed: () { onAction(stock); } onPressed: () { onAction(stock); },
onLongPressed: () { onOpen(stock); }
); );
} }
) )
......
...@@ -5,10 +5,15 @@ ...@@ -5,10 +5,15 @@
part of stocks; part of stocks;
class StockRow extends StatelessComponent { 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 Stock stock;
final GestureTapListener onPressed; final GestureTapListener onPressed;
final GestureLongPressListener onLongPressed;
static const double kHeight = 79.0; static const double kHeight = 79.0;
...@@ -39,6 +44,7 @@ class StockRow extends StatelessComponent { ...@@ -39,6 +44,7 @@ class StockRow extends StatelessComponent {
return new GestureDetector( return new GestureDetector(
onTap: onPressed, onTap: onPressed,
onLongPress: onLongPressed,
child: new InkWell( child: new InkWell(
child: new Container( child: new Container(
padding: const EdgeDims(16.0, 16.0, 20.0, 16.0), 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 { ...@@ -30,12 +30,14 @@ class App extends StatefulComponent {
Key key, Key key,
this.title, this.title,
this.theme, this.theme,
this.routes this.routes,
this.onGenerateRoute
}): super(key: key); }): super(key: key);
final String title; final String title;
final ThemeData theme; final ThemeData theme;
final Map<String, RouteBuilder> routes; final Map<String, RouteBuilder> routes;
final RouteGenerator onGenerateRoute;
AppState createState() => new AppState(); AppState createState() => new AppState();
} }
...@@ -76,7 +78,8 @@ class AppState extends State<App> { ...@@ -76,7 +78,8 @@ class AppState extends State<App> {
title: config.title, title: config.title,
child: new Navigator( child: new Navigator(
key: _navigator, key: _navigator,
routes: config.routes routes: config.routes,
onGenerateRoute: config.onGenerateRoute
) )
) )
) )
......
...@@ -62,7 +62,9 @@ class Material extends StatelessComponent { ...@@ -62,7 +62,9 @@ class Material extends StatelessComponent {
} }
} }
// TODO(abarth): This should use AnimatedContainer. // TODO(abarth): This should use AnimatedContainer.
return new Container( return new DefaultTextStyle(
style: Theme.of(context).text.body1,
child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: getBackgroundColor(context), backgroundColor: getBackgroundColor(context),
borderRadius: edges[type], borderRadius: edges[type],
...@@ -70,6 +72,7 @@ class Material extends StatelessComponent { ...@@ -70,6 +72,7 @@ class Material extends StatelessComponent {
shape: type == MaterialType.circle ? Shape.circle : Shape.rectangle shape: type == MaterialType.circle ? Shape.circle : Shape.rectangle
), ),
child: contents child: contents
)
); );
} }
} }
...@@ -9,15 +9,24 @@ import 'package:sky/src/fn3/framework.dart'; ...@@ -9,15 +9,24 @@ import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/transitions.dart'; import 'package:sky/src/fn3/transitions.dart';
typedef Widget RouteBuilder(NavigatorState navigator, Route route); typedef Widget RouteBuilder(NavigatorState navigator, Route route);
typedef RouteBuilder RouteGenerator(String name);
typedef void RouteStateCallback(RouteState route);
typedef void NotificationCallback(); typedef void NotificationCallback();
class Navigator extends StatefulComponent { 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 '/'. // To use a navigator, you must at a minimum define the route with the name '/'.
assert(routes.containsKey('/')); assert(routes.containsKey('/'));
} }
final Map<String, RouteBuilder> routes; final Map<String, RouteBuilder> routes;
final RouteGenerator onGenerateRoute;
final RouteBuilder onUnknownRoute;
NavigatorState createState() => new NavigatorState(); NavigatorState createState() => new NavigatorState();
} }
...@@ -47,9 +56,17 @@ class NavigatorState extends State<Navigator> { ...@@ -47,9 +56,17 @@ class NavigatorState extends State<Navigator> {
} }
void pushNamed(String name) { void pushNamed(String name) {
PageRoute route = new PageRoute(config.routes[name]); RouteBuilder builder;
assert(route != null); if (!config.routes.containsKey(name)) {
push(route); 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) { void push(Route route) {
...@@ -172,6 +189,10 @@ abstract class Route { ...@@ -172,6 +189,10 @@ abstract class Route {
/// NavigatorState.pushState(). /// NavigatorState.pushState().
/// ///
/// Set hasContent to false if you have nothing useful to return from build(). /// 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; bool get hasContent => true;
/// If ephemeral is true, then to explicitly pop the route you have to use /// If ephemeral is true, then to explicitly pop the route you have to use
...@@ -196,6 +217,7 @@ abstract class Route { ...@@ -196,6 +217,7 @@ abstract class Route {
/// popped. /// popped.
/// ///
/// ephemeral must be true if modal is false. /// ephemeral must be true if modal is false.
/// hasContent must be true if modal is true.
bool get modal => true; bool get modal => true;
/// If opaque is true, then routes below this one will not be built or painted /// If opaque is true, then routes below this one will not be built or painted
...@@ -253,8 +275,6 @@ class PageRoute extends Route { ...@@ -253,8 +275,6 @@ class PageRoute extends Route {
} }
} }
typedef void RouteStateCallback(RouteState route);
class RouteState extends Route { class RouteState extends Route {
RouteState({ this.route, this.owner, this.callback }); RouteState({ this.route, this.owner, this.callback });
...@@ -262,6 +282,8 @@ class RouteState extends Route { ...@@ -262,6 +282,8 @@ class RouteState extends Route {
State owner; State owner;
RouteStateCallback callback; RouteStateCallback callback;
bool get hasContent => false;
bool get modal => false;
bool get opaque => false; bool get opaque => false;
void didPop([dynamic result]) { void didPop([dynamic result]) {
...@@ -271,6 +293,5 @@ class RouteState extends Route { ...@@ -271,6 +293,5 @@ class RouteState extends Route {
super.didPop(result); super.didPop(result);
} }
bool get hasContent => false;
Widget build(Key key, NavigatorState navigator) => null; Widget build(Key key, NavigatorState navigator) => null;
} }
...@@ -167,7 +167,7 @@ class MenuRoute extends Route { ...@@ -167,7 +167,7 @@ class MenuRoute extends Route {
return result; 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; bool get modal => true;
Duration get transitionDuration => _kMenuDuration; 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