mirror of
https://github.com/flutter/flutter.git
synced 2026-02-06 03:39:05 +08:00
Port stocks to fn3 and introduce an App component.
This commit is contained in:
parent
93f1ba5dac
commit
0a0a92eb45
@ -10,7 +10,8 @@ import 'dart:sky' as sky;
|
||||
|
||||
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_row.dart';
|
||||
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 @@ class StocksApp extends App {
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@ -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 @@ class StockArrow extends Component {
|
||||
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 @@ class StockArrow extends Component {
|
||||
margin: const EdgeDims.symmetric(horizontal: 5.0)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,20 +9,17 @@ 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);
|
||||
|
||||
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 @@ class StockHome extends StatefulComponent {
|
||||
bool _isSnackBarShowing = false;
|
||||
|
||||
void _handleSearchBegin() {
|
||||
navigator.pushState(this, (_) {
|
||||
config.navigator.pushState(this, (_) {
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
_searchQuery = null;
|
||||
@ -43,9 +40,9 @@ class StockHome extends StatefulComponent {
|
||||
}
|
||||
|
||||
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 @@ class StockHome extends StatefulComponent {
|
||||
}
|
||||
|
||||
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 @@ class StockHome extends StatefulComponent {
|
||||
level: 3,
|
||||
showing: _drawerShowing,
|
||||
onDismissed: _handleDrawerDismissed,
|
||||
navigator: navigator,
|
||||
navigator: config.navigator,
|
||||
children: [
|
||||
new DrawerHeader(child: new Text('Stocks')),
|
||||
new DrawerItem(
|
||||
@ -122,7 +116,7 @@ class StockHome extends StatefulComponent {
|
||||
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 @@ class StockHome extends StatefulComponent {
|
||||
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 @@ class StockHome extends StatefulComponent {
|
||||
}
|
||||
|
||||
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 @@ class StockHome extends StatefulComponent {
|
||||
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 @@ class StockHome extends StatefulComponent {
|
||||
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 @@ class StockHome extends StatefulComponent {
|
||||
placeholder: 'Search stocks',
|
||||
onChanged: _handleSearchQueryChanged
|
||||
),
|
||||
backgroundColor: Theme.of(this).canvasColor
|
||||
backgroundColor: Theme.of(context).canvasColor
|
||||
);
|
||||
}
|
||||
|
||||
@ -255,17 +252,14 @@ class StockHome extends StatefulComponent {
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 @@ Future showStockMenu(Navigator navigator, { bool autorefresh, ValueChanged onAut
|
||||
),
|
||||
];
|
||||
}
|
||||
);
|
||||
)) {
|
||||
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.
|
||||
}
|
||||
}
|
||||
@ -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 @@ class StockRow extends Component {
|
||||
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 @@ class StockRow extends Component {
|
||||
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 @@ class StockRow extends Component {
|
||||
child: new Row(
|
||||
children,
|
||||
alignItems: FlexAlignItems.baseline,
|
||||
textBaseline: DefaultTextStyle.of(this).textBaseline
|
||||
textBaseline: DefaultTextStyle.of(context).textBaseline
|
||||
)
|
||||
)
|
||||
])
|
||||
|
||||
@ -10,42 +10,32 @@ typedef void SettingsUpdater({
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
void syncConstructorArguments(StockSettings source) {
|
||||
navigator = source.navigator;
|
||||
optimism = source.optimism;
|
||||
backup = source.backup;
|
||||
updater = source.updater;
|
||||
}
|
||||
StockSettingsState createState() => new StockSettingsState();
|
||||
}
|
||||
|
||||
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 @@ class StockSettings extends StatefulComponent {
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ class StockSettings extends StatefulComponent {
|
||||
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 @@ class StockSettings extends StatefulComponent {
|
||||
);
|
||||
}
|
||||
|
||||
Widget build() {
|
||||
Widget build(BuildContext context) {
|
||||
return new Scaffold(
|
||||
toolbar: buildToolBar(),
|
||||
body: buildSettingsPane()
|
||||
toolbar: buildToolBar(context),
|
||||
body: buildSettingsPane(context)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
83
packages/flutter/lib/src/fn3/app.dart
Normal file
83
packages/flutter/lib/src/fn3/app.dart
Normal file
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -44,10 +44,11 @@ class Checkbox extends StatelessComponent {
|
||||
? _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 @@ class Checkbox extends StatelessComponent {
|
||||
// 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 @@ class _CheckboxWrapper extends LeafRenderObjectWidget {
|
||||
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 _CheckboxWrapper extends LeafRenderObjectWidget {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import 'package:sky/src/fn3/scrollable.dart';
|
||||
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 @@ class Dialog extends StatelessComponent {
|
||||
|
||||
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
|
||||
|
||||
class DialogRoute extends RouteBase {
|
||||
class DialogRoute extends Route {
|
||||
DialogRoute({ this.completer, this.builder });
|
||||
|
||||
final Completer completer;
|
||||
|
||||
@ -8,11 +8,11 @@ import 'package:sky/src/fn3/focus.dart';
|
||||
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 @@ abstract class RouteBase {
|
||||
|
||||
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 @@ class Route extends RouteBase {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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 @@ class RouteState extends RouteBase {
|
||||
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);
|
||||
|
||||
@ -6,8 +6,8 @@ import 'dart:async';
|
||||
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/gesture_detector.dart';
|
||||
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 _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
|
||||
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 PopupMenu extends StatefulComponent {
|
||||
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 @@ class PopupMenuState extends State<PopupMenu> {
|
||||
});
|
||||
}
|
||||
|
||||
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 @@ class MenuPosition {
|
||||
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 @@ class MenuRoute extends RouteBase {
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@ -63,6 +63,7 @@ class Typography {
|
||||
// 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',
|
||||
|
||||
@ -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 @@ class BindingHitTestEntry extends HitTestEntry {
|
||||
}
|
||||
|
||||
/// 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 }) {
|
||||
|
||||
@ -50,18 +50,12 @@ void main() {
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user