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';
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,14 +62,17 @@ class Material extends StatelessComponent {
}
}
// TODO(abarth): This should use AnimatedContainer.
return new Container(
decoration: new BoxDecoration(
backgroundColor: getBackgroundColor(context),
borderRadius: edges[type],
boxShadow: level == 0 ? null : shadows[level],
shape: type == MaterialType.circle ? Shape.circle : Shape.rectangle
),
child: contents
return new DefaultTextStyle(
style: Theme.of(context).text.body1,
child: new Container(
decoration: new BoxDecoration(
backgroundColor: getBackgroundColor(context),
borderRadius: edges[type],
boxShadow: level == 0 ? null : shadows[level],
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