Commit 309b9f80 authored by Hans Muller's avatar Hans Muller

Version 0.0 of a gallery demo of the Material Design "Shrine" app (#4327)

parent a4640521
......@@ -6,7 +6,7 @@ dependencies:
path: ../../../packages/flutter
flutter_driver:
path: ../../../packages/flutter_driver
flutter_gallery_assets: '0.0.16'
flutter_gallery_assets: '0.0.18'
dev_dependencies:
flutter_test:
......
......@@ -37,5 +37,8 @@ assets:
- packages/flutter_gallery_assets/landscape_9.jpg
- packages/flutter_gallery_assets/landscape_10.jpg
- packages/flutter_gallery_assets/landscape_11.jpg
- packages/flutter_gallery_assets/shadow.png
- lib/gallery/example_code.dart
fonts:
- family: AbrilFatface
fonts:
- asset: packages/flutter_gallery_assets/shrine/fonts/abrilfatface/AbrilFatface-Regular.ttf
......@@ -25,6 +25,7 @@ export 'persistent_bottom_sheet_demo.dart';
export 'progress_indicator_demo.dart';
export 'scrollable_tabs_demo.dart';
export 'selection_controls_demo.dart';
export 'shrine_demo.dart';
export 'slider_demo.dart';
export 'snack_bar_demo.dart';
export 'tabs_demo.dart';
......
This diff is collapsed.
// Copyright 2016 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:async';
import 'dart:collection' show HashSet;
import 'package:flutter/material.dart';
import 'shrine_data.dart';
import 'shrine_order.dart';
import 'shrine_page.dart';
import 'shrine_theme.dart';
import 'shrine_types.dart';
const double unitSize = kToolBarHeight;
Map<Product, Order> shoppingCart = <Product, Order>{};
/// Displays the Vendor's name and avatar.
class VendorItem extends StatelessWidget {
VendorItem({ Key key, this.vendor }) : super(key: key) {
assert(vendor != null);
}
final Vendor vendor;
@override
Widget build(BuildContext context) {
return new SizedBox(
height: 24.0,
child: new Row(
children: <Widget>[
new SizedBox(
width: 24.0,
child: new ClipRRect(
xRadius: 12.0,
yRadius: 12.0,
child: new NetworkImage(
fit: ImageFit.cover,
src: vendor.avatarUrl
)
)
),
new SizedBox(width: 8.0),
new Flexible(
child: new Text(vendor.name, style: ShrineTheme.of(context).vendorItemStyle)
)
]
)
);
}
}
/// Displays the product's price. If the product is in the shopping cart the background
/// is highlighted.
class PriceItem extends StatelessWidget {
PriceItem({ Key key, this.product }) : super(key: key) {
assert(product != null);
}
final Product product;
@override
Widget build(BuildContext context) {
BoxDecoration decoration;
if (shoppingCart[product] != null)
decoration = new BoxDecoration(backgroundColor: const Color(0xFFFFE0E0));
return new Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
decoration: decoration,
child: new Text(product.priceString, style: ShrineTheme.of(context).priceStyle)
);
}
}
/// Layout the main left and right elements of a FeatureItem.
class FeatureLayout extends MultiChildLayoutDelegate {
FeatureLayout();
static final String left = 'left';
static final String right = 'right';
// Horizontally: the feature product image appears on the left and
// occupies 50% of the available width; the feature product's
// description apepars on the right and occupies 50% of the available
// width + unitSize. The left and right widgets overlap and the right
// widget is stacked on top.
@override
void performLayout(Size size) {
final double halfWidth = size.width / 2.0;
layoutChild(left, new BoxConstraints.tightFor(width: halfWidth, height: size.height));
positionChild(left, Offset.zero);
layoutChild(right, new BoxConstraints.expand(width: halfWidth + unitSize, height: size.height));
positionChild(right, new Offset(halfWidth - unitSize, 0.0));
}
@override
bool shouldRelayout(FeatureLayout oldDelegate) => false;
}
/// A card that highlights the "featured" catalog item.
class FeatureItem extends StatelessWidget {
FeatureItem({ Key key, this.product }) : super(key: key) {
assert(product.featureTitle != null);
assert(product.featureDescription != null);
}
final Product product;
@override
Widget build(BuildContext context) {
final ShrineTheme theme = ShrineTheme.of(context);
return new AspectRatio(
aspectRatio: 3.0 / 3.5,
child: new Material(
type: MaterialType.card,
elevation: 1,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new SizedBox(
height: unitSize,
child: new Align(
alignment: FractionalOffset.topRight,
child: new PriceItem(product: product)
)
),
new Flexible(
child: new CustomMultiChildLayout(
delegate: new FeatureLayout(),
children: <Widget>[
new LayoutId(
id: FeatureLayout.left,
child: new ClipRect(
child: new OverflowBox(
minWidth: 340.0,
maxWidth: 340.0,
minHeight: 340.0,
maxHeight: 340.0,
alignment: FractionalOffset.topRight,
child: new NetworkImage(
fit: ImageFit.cover,
src: product.imageUrl
)
)
)
),
new LayoutId(
id: FeatureLayout.right,
child: new Padding(
padding: const EdgeInsets.only(right: 16.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Padding(
padding: const EdgeInsets.only(top: 18.0),
child: new Text(product.featureTitle, style: theme.featureTitleStyle)
),
new Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: new Text(product.featureDescription, style: theme.featureStyle)
),
new VendorItem(vendor: product.vendor)
]
)
)
)
]
)
)
]
)
)
);
}
}
/// A card that displays a product's image, price, and vendor.
class ProductItem extends StatelessWidget {
ProductItem({ Key key, this.product, this.onPressed }) : super(key: key) {
assert(product != null);
}
final Product product;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return new Card(
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new Column(
children: <Widget>[
new Align(
alignment: FractionalOffset.centerRight,
child: new PriceItem(product: product)
),
new SizedBox(
width: 144.0,
height: 144.0,
child: new Stack(
children: <Widget>[
new Hero(
tag: productHeroTag,
key: new ObjectKey(product),
child: new NetworkImage(
fit: ImageFit.contain,
src: product.imageUrl
)
),
new Material(
color: Theme.of(context).canvasColor.withAlpha(0x00),
child: new InkWell(onTap: onPressed)
),
]
)
),
new VendorItem(vendor: product.vendor)
]
)
)
);
}
}
/// The Shrine app's home page. Displays the featured item above all of the
/// product items arranged in two columns.
class ShrineHome extends StatefulWidget {
@override
_ShrineHomeState createState() => new _ShrineHomeState();
}
class _ShrineHomeState extends State<ShrineHome> {
final List<Product> _products = allProducts();
void handleCompletedOrder(Order completedOrder) {
assert(completedOrder.product != null);
if (completedOrder.inCart && completedOrder.quantity > 0)
shoppingCart[completedOrder.product] = completedOrder;
else
shoppingCart[completedOrder.product] = null;
}
void showOrderPage(Product product) {
final Order order = shoppingCart[product] ?? new Order(product: product);
final Completer<Order> completer = new Completer<Order>();
final Key productKey = new ObjectKey(product);
final Set<Key> mostValuableKeys = new HashSet<Key>();
mostValuableKeys.add(productKey);
Navigator.push(context, new ShrineOrderRoute(
order: order,
settings: new RouteSettings(mostValuableKeys: mostValuableKeys),
completer: completer,
builder: (BuildContext context) {
return new OrderPage(
order: order,
products: _products
);
}
));
completer.future.then(handleCompletedOrder);
}
@override
Widget build(BuildContext context) {
final Product featured = _products.firstWhere((Product product) => product.featureDescription != null);
return new ShrinePage(
body: new ScrollableViewport(
child: new Column(
children: <Widget>[
new Container(
margin: new EdgeInsets.only(bottom: 8.0),
child: new FeatureItem(product: featured)
),
new FixedColumnCountGrid(
columnCount: 2,
rowSpacing: 8.0,
columnSpacing: 8.0,
padding: const EdgeInsets.all(8.0),
tileAspectRatio: 160.0 / 216.0, // width/height
children: _products.map((Product product) {
return new ProductItem(
product: product,
onPressed: () { showOrderPage(product); }
);
}).toList()
)
]
)
)
);
}
}
// Copyright 2016 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:async';
import 'package:flutter/material.dart';
import '../shrine_demo.dart' show ShrinePageRoute;
import 'shrine_page.dart';
import 'shrine_theme.dart';
import 'shrine_types.dart';
/// Describes a product and vendor in detail, supports specifying
/// a order quantity (0-5). Appears at the top of the OrderPage.
class OrderItem extends StatelessWidget {
OrderItem({ Key key, this.product, this.quantity, this.quantityChanged }) : super(key: key) {
assert(product != null);
assert(quantity != null && quantity >= 0 && quantity <= 5);
}
final Product product;
final int quantity;
final ValueChanged<int> quantityChanged;
@override
Widget build(BuildContext context) {
final ShrineTheme theme = ShrineTheme.of(context);
return new Material(
type: MaterialType.card,
elevation: 0,
child: new Padding(
padding: const EdgeInsets.only(left: 16.0, top: 18.0, right: 16.0, bottom: 24.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Padding(
padding: const EdgeInsets.only(left: 56.0),
child: new SizedBox(
width: 248.0,
height: 248.0,
child: new Hero(
tag: productHeroTag,
child: new NetworkImage(
fit: ImageFit.contain,
src: product.imageUrl
)
)
)
),
new SizedBox(height: 24.0),
new Row(
children: <Widget>[
new Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: new Center(
child: new Icon(
icon: Icons.info_outline,
size: 24.0,
color: const Color(0xFFFFE0E0)
)
)
),
new Flexible(
child: new Text(product.name, style: theme.featureTitleStyle)
)
]
),
new Padding(
padding: const EdgeInsets.only(left: 56.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new SizedBox(height: 24.0),
new Text(product.description, style: theme.featureStyle),
new SizedBox(height: 16.0),
new Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 88.0),
child: new DropDownButtonHideUnderline(
child: new Container(
decoration: new BoxDecoration(
border: new Border.all(
color: const Color(0xFFD9D9D9)
)
),
child: new DropDownButton<int>(
items: <int>[0, 1, 2, 3, 4, 5].map((int value) {
return new DropDownMenuItem<int>(
value: value,
child: new Text('Quantity $value', style: theme.quantityMenuStyle)
);
}).toList(),
value: quantity,
onChanged: quantityChanged
)
)
)
),
new SizedBox(height: 16.0),
new SizedBox(
height: 24.0,
child: new Align(
alignment: FractionalOffset.bottomLeft,
child: new Text(product.vendor.name, style: theme.vendorTitleStyle)
)
),
new SizedBox(height: 16.0),
new Text(product.vendor.description, style: theme.vendorStyle),
new SizedBox(height: 24.0)
]
)
)
]
)
)
);
}
}
class OrderPage extends StatefulWidget {
OrderPage({ Key key, this.order, this.products }) : super(key: key) {
assert(order != null);
assert(products != null && products.length > 0);
}
final Order order;
final List<Product> products;
@override
_OrderPageState createState() => new _OrderPageState();
}
/// Displays a product's OrderItem above photos of all of the other products
/// arranged in two columns. Enables the user to specify a quantity and add an
/// order to the shopping cart.
class _OrderPageState extends State<OrderPage> {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Order Page');
Order get currentOrder => ShrineOrderRoute.of(context).order;
set currentOrder(Order value) {
ShrineOrderRoute.of(context).order = value;
}
void updateOrder({ int quantity, bool inCart }) {
Order newOrder = currentOrder.copyWith(quantity: quantity, inCart: inCart);
if (currentOrder != newOrder) {
setState(() {
currentOrder = newOrder;
});
}
}
void showSnackBarMessage(String message) {
scaffoldKey.currentState.showSnackBar(new SnackBar(content: new Text(message)));
}
@override
Widget build(BuildContext context) {
return new ShrinePage(
scaffoldKey: scaffoldKey,
floatingActionButton: new FloatingActionButton(
onPressed: () {
updateOrder(inCart: true);
showSnackBarMessage('There are ${currentOrder.quantity} items in the shopping cart');
},
backgroundColor: const Color(0xFF16F0F0),
child: new Icon(
icon: Icons.add_shopping_cart,
color: Colors.black
)
),
body: new Block(
children: <Widget>[
new OrderItem(
product: config.order.product,
quantity: currentOrder.quantity,
quantityChanged: (int value) { updateOrder(quantity: value); }
),
new SizedBox(height: 24.0),
new FixedColumnCountGrid(
columnCount: 2,
rowSpacing: 8.0,
columnSpacing: 8.0,
padding: const EdgeInsets.all(8.0),
tileAspectRatio: 160.0 / 216.0, // width/height
children: config.products
.where((Product product) => product != config.order.product)
.map((Product product) {
return new Card(
elevation: 0,
child: new NetworkImage(
fit: ImageFit.contain,
src: product.imageUrl
)
);
}).toList()
)
]
)
);
}
}
/// Displays a full-screen modal OrderPage.
///
/// The order field will be replaced each time the user reconfigures the order.
/// When the user backs out of this route the completer's value will be the
/// final value of the order field.
class ShrineOrderRoute extends ShrinePageRoute<Order> {
ShrineOrderRoute({
this.order,
WidgetBuilder builder,
Completer<Order> completer,
RouteSettings settings: const RouteSettings()
}) : super(builder: builder, completer: completer, settings: settings) {
assert(order != null);
}
Order order;
@override
Order get currentResult => order;
static ShrineOrderRoute of(BuildContext context) => ModalRoute.of(context);
}
// Copyright 2016 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 'package:flutter/material.dart';
import 'shrine_theme.dart';
enum ShrineAction {
sortByPrice,
sortByProduct,
emptyCart
}
/// Defines the Scaffold, AppBar, etc that the demo pages have in common.
class ShrinePage extends StatelessWidget {
ShrinePage({ Key key, this.scaffoldKey, this.body, this.floatingActionButton }) : super(key: key);
final Key scaffoldKey;
final Widget body;
final Widget floatingActionButton;
@override
Widget build(BuildContext context) {
return new Scaffold(
key: scaffoldKey,
appBar: new AppBar(
title: new Center(
child: new Text('SHRINE', style: ShrineTheme.of(context).appBarTitleStyle)
),
backgroundColor: Theme.of(context).canvasColor,
actions: <Widget>[ // TODO(hansmuller): implement the actions.
new IconButton(
icon: Icons.shopping_cart,
tooltip: 'Shopping cart',
onPressed: () { /* activate the button for now */ }
),
new PopupMenuButton<ShrineAction>(
itemBuilder: (BuildContext context) => <PopupMenuItem<ShrineAction>>[
new PopupMenuItem<ShrineAction>(
value: ShrineAction.sortByPrice,
child: new Text('Sort by price')
),
new PopupMenuItem<ShrineAction>(
value: ShrineAction.sortByProduct,
child: new Text('Sort by product')
),
new PopupMenuItem<ShrineAction>(
value: ShrineAction.emptyCart,
child: new Text('Empty shopping cart')
)
]
)
]
),
floatingActionButton: floatingActionButton,
body: body
);
}
}
// Copyright 2016 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 'package:flutter/material.dart';
class ShrineStyle extends TextStyle {
const ShrineStyle.roboto(double size, FontWeight weight, Color color)
: super(inherit: false, color: color, fontSize: size, fontWeight: weight, textBaseline: TextBaseline.alphabetic);
const ShrineStyle.abrilFatface(double size, FontWeight weight, Color color)
: super(inherit: false, color: color, fontFamily: 'AbrilFatface', fontSize: size, fontWeight: weight, textBaseline: TextBaseline.alphabetic);
}
TextStyle robotoRegular12(Color color) => new ShrineStyle.roboto(12.0, FontWeight.w500, color);
TextStyle robotoLight12(Color color) => new ShrineStyle.roboto(12.0, FontWeight.w300, color);
TextStyle robotoRegular14(Color color) => new ShrineStyle.roboto(14.0, FontWeight.w500, color);
TextStyle robotoMedium14(Color color) => new ShrineStyle.roboto(14.0, FontWeight.w600, color);
TextStyle robotoLight14(Color color) => new ShrineStyle.roboto(14.0, FontWeight.w300, color);
TextStyle robotoRegular20(Color color) => new ShrineStyle.roboto(20.0, FontWeight.w500, color);
TextStyle abrilFatfaceRegular24(Color color) => new ShrineStyle.abrilFatface(24.0, FontWeight.w500, color);
TextStyle abrilFatfaceRegular34(Color color) => new ShrineStyle.abrilFatface(34.0, FontWeight.w500, color);
/// The TextStyles and Colors used for titles, labels, and descriptions. This
/// InheritedWidget is shared by all of the routes and widgets created for
/// the Shrine app.
class ShrineTheme extends InheritedWidget {
ShrineTheme({ Key key, Widget child }) : super(key: key, child: child) {
assert(child != null);
}
final TextStyle appBarTitleStyle = robotoRegular20(Colors.black87);
final TextStyle vendorItemStyle = robotoRegular12(const Color(0xFF81959D));
final TextStyle priceStyle = robotoRegular14(Colors.black87);
final TextStyle featureTitleStyle = abrilFatfaceRegular34(Colors.black87);
final TextStyle featureStyle = robotoLight14(Colors.black54);
final TextStyle orderTitleStyle = abrilFatfaceRegular24(Colors.black87);
final TextStyle orderStyle = robotoLight14(Colors.black54);
final TextStyle vendorTitleStyle = robotoMedium14(Colors.black87);
final TextStyle vendorStyle = robotoLight14(Colors.black54);
final TextStyle quantityMenuStyle = robotoLight14(Colors.black54);
static ShrineTheme of(BuildContext context) => context.inheritFromWidgetOfExactType(ShrineTheme);
@override
bool updateShouldNotify(ShrineTheme old) => false;
}
// Copyright 2016 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:ui' show hashValues;
const String productHeroTag = 'Product';
class Vendor {
const Vendor({
this.name,
this.description,
this.avatarUrl
});
final String name;
final String description;
final String avatarUrl;
bool isValid() {
return name != null &&
description != null &&
avatarUrl != null;
}
@override
String toString() => 'Vendor($name)';
}
class Product {
const Product({
this.name,
this.description,
this.featureTitle,
this.featureDescription,
this.imageUrl,
this.categories,
this.price,
this.vendor
});
final String name;
final String description;
final String featureTitle;
final String featureDescription;
final String imageUrl;
final List<String> categories;
final double price;
final Vendor vendor;
String get priceString => '\$${price.floor()}';
bool isValid() {
return name != null &&
description != null &&
imageUrl != null &&
categories != null &&
categories.length > 0 &&
price != null &&
vendor.isValid();
}
@override
String toString() => 'Product($name)';
}
class Order {
Order({ this.product, this.quantity: 1, this.inCart: false }) {
assert(product != null);
assert(quantity != null && quantity >= 0);
assert(inCart != null);
}
final Product product;
final int quantity;
final bool inCart;
Order copyWith({ Product product, int quantity, bool inCart }) {
return new Order(
product: product ?? this.product,
quantity: quantity ?? this.quantity,
inCart: inCart ?? this.inCart
);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
final Order typedOther = other;
return product == typedOther.product &&
quantity == typedOther.quantity &&
inCart == typedOther.inCart;
}
@override
int get hashCode => hashValues(product, quantity, inCart);
@override
String toString() => 'Order($product, quantity=$quantity, inCart=$inCart)';
}
// Copyright 2016 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:async';
import 'package:flutter/material.dart';
import 'shrine/shrine_home.dart' show ShrineHome;
import 'shrine/shrine_theme.dart' show ShrineTheme;
// This code would ordinarily be part of the MaterialApp's home. It's being
// used by the ShrineDemo and by each route pushed from there because this
// isn't a standalone app with its own main() and MaterialApp.
Widget buildShrine(Widget child) {
return new Theme(
data: new ThemeData(primarySwatch: Colors.grey),
child: new IconTheme(
data: new IconThemeData(color: const Color(0xFF707070)),
child: new ShrineTheme(
child: child
)
)
);
}
// In a standalone version of this app, MaterialPageRoute<T> could be used directly.
class ShrinePageRoute<T> extends MaterialPageRoute<T> {
ShrinePageRoute({
WidgetBuilder builder,
Completer<T> completer,
RouteSettings settings: const RouteSettings()
}) : super(builder: builder, completer: completer, settings: settings);
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
return buildShrine(super.buildPage(context, animation, forwardAnimation));
}
}
class ShrineDemo extends StatelessWidget {
static const String routeName = '/shrine'; // Used by the Gallery app.
@override
Widget build(BuildContext context) => buildShrine(new ShrineHome());
}
......@@ -9,6 +9,7 @@ import '../demo/all.dart';
import 'home.dart';
final Map<String, WidgetBuilder> kRoutes = <String, WidgetBuilder>{
ShrineDemo.routeName: (BuildContext context) => new ShrineDemo(),
WeatherDemo.routeName: (BuildContext context) => new WeatherDemo(),
FitnessDemo.routeName: (BuildContext context) => new FitnessDemo(),
Calculator.routeName: (BuildContext context) => new Calculator(),
......
......@@ -71,6 +71,7 @@ class GalleryHomeState extends State<GalleryHome> {
leading: new Icon(icon: Icons.star),
title: new Text('Demos'),
children: <Widget>[
new GalleryItem(title: 'Shrine', routeName: ShrineDemo.routeName),
new GalleryItem(title: 'Weather', routeName: WeatherDemo.routeName),
new GalleryItem(title: 'Fitness', routeName: FitnessDemo.routeName),
new GalleryItem(title: 'Calculator', routeName: Calculator.routeName),
......
......@@ -10,7 +10,7 @@ dependencies:
path: ../../packages/flutter_sprites
flutter_markdown:
path: ../../packages/flutter_markdown
flutter_gallery_assets: '0.0.16'
flutter_gallery_assets: '0.0.18'
dev_dependencies:
test: any # flutter_test provides the version constraints
......
......@@ -183,7 +183,7 @@ class AppBar extends StatelessWidget {
final double statusBarHeight = MediaQuery.of(context).padding.top;
final ThemeData theme = Theme.of(context);
IconThemeData iconTheme = theme.primaryIconTheme;
IconThemeData iconTheme = IconTheme.of(context) ?? theme.primaryIconTheme;
TextStyle centerStyle = textTheme?.title ?? theme.primaryTextTheme.title;
TextStyle sideStyle = textTheme?.body1 ?? theme.primaryTextTheme.body1;
......
......@@ -17,8 +17,9 @@ class Card extends StatelessWidget {
/// Creates a material design card.
const Card({
Key key,
this.child,
this.color
this.color,
this.elevation: 2,
this.child
}) : super(key: key);
/// The widget below this widget in the tree.
......@@ -27,6 +28,9 @@ class Card extends StatelessWidget {
/// The color of material used for this card.
final Color color;
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
final int elevation;
@override
Widget build(BuildContext context) {
return new Container(
......@@ -34,7 +38,7 @@ class Card extends StatelessWidget {
child: new Material(
color: color,
type: MaterialType.card,
elevation: 2,
elevation: elevation,
child: child
)
);
......
......@@ -32,6 +32,10 @@ abstract class Route<T> {
/// Called after install() when the route is pushed onto the navigator.
void didPush() { }
/// When this route is popped (see [Navigator.pop]) if the result isn't
/// specified or if it's null, this value will be used instead.
T get currentResult => null;
/// Called after install() when the route replaced another in the navigator.
void didReplace(Route<dynamic> oldRoute) { }
......@@ -427,7 +431,7 @@ class NavigatorState extends State<Navigator> {
assert(route._navigator == this);
bool debugPredictedWouldPop;
assert(() { debugPredictedWouldPop = !route.willHandlePopInternally; return true; });
if (route.didPop(result)) {
if (route.didPop(result ?? route.currentResult)) {
assert(debugPredictedWouldPop);
if (_history.length > 1) {
setState(() {
......@@ -564,11 +568,12 @@ class NavigatorTransaction {
/// (if any) is notified using its didPop() method, and the previous route is
/// notified using [Route.didChangeNext].
///
/// If non-null, [result] will be used as the result of the route. Routes
/// such as dialogs or popup menus typically use this mechanism to return the
/// value selected by the user to the widget that created their route. The
/// type of [result], if provided, must match the type argument of the class
/// of the current route. (In practice, this is usually "dynamic".)
/// If non-null, [result] will be used as the result of the route, otherwise
/// the route's [Route.currentValue] will be used. Routes such as dialogs or
/// popup menus typically use this mechanism to return the value selected by
/// the user to the widget that created their route. The type of [result],
/// if provided, must match the type argument of the class of the current
/// route. (In practice, this is usually "dynamic".)
///
/// Returns true if a route was popped; returns false if there are no further
/// previous routes.
......
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