diff --git a/avon_farm_foods.iml b/avon_farm_foods.iml
index a700c39..0d882de 100644
--- a/avon_farm_foods.iml
+++ b/avon_farm_foods.iml
@@ -11,6 +11,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/avon_farm_foods_android.iml b/avon_farm_foods_android.iml
index 0ca70ed..b9d5740 100644
--- a/avon_farm_foods_android.iml
+++ b/avon_farm_foods_android.iml
@@ -19,8 +19,8 @@
-
+
-
+
\ No newline at end of file
diff --git a/images/home.jpg b/images/home.jpg
new file mode 100644
index 0000000..899636c
Binary files /dev/null and b/images/home.jpg differ
diff --git a/images/splash_screen.png b/images/splash_screen.png
new file mode 100644
index 0000000..65d0ade
Binary files /dev/null and b/images/splash_screen.png differ
diff --git a/lib/enums/clear_basket_dialog_action.dart b/lib/enums/clear_basket_dialog_action.dart
new file mode 100644
index 0000000..76e0a20
--- /dev/null
+++ b/lib/enums/clear_basket_dialog_action.dart
@@ -0,0 +1,4 @@
+enum ClearBasketDialogAction {
+ no,
+ yes,
+}
diff --git a/lib/enums/theme.dart b/lib/enums/theme.dart
new file mode 100644
index 0000000..a444f80
--- /dev/null
+++ b/lib/enums/theme.dart
@@ -0,0 +1,4 @@
+enum AppTheme {
+ dark,
+ light,
+}
diff --git a/lib/main.dart b/lib/main.dart
index 9c0a43f..8acd3c6 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,15 +1,108 @@
+import 'dart:convert';
+
+import 'package:avon_farm_foods/enums/theme.dart';
+import 'package:avon_farm_foods/models/configuration.dart';
+import 'package:avon_farm_foods/pages/basket.dart';
+import 'package:avon_farm_foods/pages/checkout.dart';
import 'package:avon_farm_foods/pages/home.dart';
+import 'package:avon_farm_foods/pages/orders.dart';
+import 'package:avon_farm_foods/pages/products.dart';
+import 'package:avon_farm_foods/pages/settings.dart';
import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+void main() {
+ runApp(
+ new FutureBuilder(
+ builder: (
+ BuildContext context,
+ AsyncSnapshot snapshot,
+ ) {
+ return snapshot.hasData ? new MyApp(snapshot.data) : new SplashScreen();
+ },
+ future: SharedPreferences.getInstance(),
+ ),
+ );
+}
+
+class MyApp extends StatefulWidget {
+ MyApp(this.preferences);
+
+ final SharedPreferences preferences;
+
+ @override
+ _MyAppState createState() => new _MyAppState();
+}
+
+class _MyAppState extends State {
+ Configuration get _configuration {
+ if (widget.preferences.getString('configuration') == null) {
+ Configuration configuration = new Configuration();
+
+ _configurationUpdater(configuration);
+
+ return configuration;
+ } else {
+ return new Configuration.fromJson(
+ JSON.decode(
+ widget.preferences.getString('configuration'),
+ ),
+ );
+ }
+ }
+
+ ThemeData get _theme {
+ switch (_configuration.theme) {
+ case AppTheme.dark:
+ return new ThemeData.dark();
+ case AppTheme.light:
+ return new ThemeData.light();
+ default:
+ return null;
+ }
+ }
-void main() => runApp(new MyApp());
+ void _configurationUpdater(Configuration configuration) {
+ setState(() {
+ widget.preferences.setString(
+ 'configuration',
+ JSON.encode(configuration),
+ );
+ });
+ }
-class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new HomePage(),
- theme: new ThemeData.dark(),
+ routes: {
+ '/basket': (BuildContext context) => new BasketPage(),
+ '/checkout': (BuildContext context) => new CheckoutPage(),
+ '/orders': (BuildContext context) => new OrdersPage(),
+ '/products': (BuildContext context) => new ProductsPage(),
+ '/settings': (BuildContext context) {
+ return new SettingsPage(
+ _configuration,
+ _configurationUpdater,
+ );
+ },
+ },
+ theme: _theme,
title: 'Avon Farm Foods',
);
}
}
+
+class SplashScreen extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return new MaterialApp(
+ home: new Container(
+ child: new Image.asset(
+ 'images/splash_screen.png',
+ fit: BoxFit.cover,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/models/checkout.dart b/lib/models/checkout.dart
new file mode 100644
index 0000000..1001bd8
--- /dev/null
+++ b/lib/models/checkout.dart
@@ -0,0 +1,10 @@
+class Checkout {
+ String address;
+ int cardNumber;
+ int cvv;
+ String expiryDate;
+ String firstName;
+ String lastName;
+ String postcode;
+ int telephone;
+}
diff --git a/lib/models/configuration.dart b/lib/models/configuration.dart
new file mode 100644
index 0000000..ad6f589
--- /dev/null
+++ b/lib/models/configuration.dart
@@ -0,0 +1,19 @@
+import 'package:avon_farm_foods/enums/theme.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'configuration.g.dart';
+
+@JsonSerializable()
+class Configuration extends Object with _$ConfigurationSerializerMixin {
+ Configuration({this.theme = AppTheme.dark}) : assert(theme != null);
+
+ final AppTheme theme;
+
+ factory Configuration.fromJson(Map json) {
+ return _$ConfigurationFromJson(json);
+ }
+
+ Configuration copyWith({AppTheme theme}) {
+ return new Configuration(theme: theme ?? this.theme);
+ }
+}
diff --git a/lib/models/configuration.g.dart b/lib/models/configuration.g.dart
new file mode 100644
index 0000000..24766e3
--- /dev/null
+++ b/lib/models/configuration.g.dart
@@ -0,0 +1,21 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'configuration.dart';
+
+// **************************************************************************
+// Generator: JsonSerializableGenerator
+// **************************************************************************
+
+Configuration _$ConfigurationFromJson(Map json) =>
+ new Configuration(
+ theme: json['theme'] == null
+ ? null
+ : AppTheme.values.singleWhere(
+ (x) => x.toString() == "AppTheme.${json['theme']}"));
+
+abstract class _$ConfigurationSerializerMixin {
+ AppTheme get theme;
+ Map toJson() => {
+ 'theme': theme == null ? null : theme.toString().split('.')[1]
+ };
+}
diff --git a/lib/models/order.dart b/lib/models/order.dart
new file mode 100644
index 0000000..063d400
--- /dev/null
+++ b/lib/models/order.dart
@@ -0,0 +1,11 @@
+import 'package:avon_farm_foods/models/product.dart';
+
+class Order {
+ Order({this.id, this.date, this.delivery, this.products, this.total});
+
+ final int id;
+ final DateTime date;
+ final DateTime delivery;
+ final List products;
+ final double total;
+}
diff --git a/lib/models/page.dart b/lib/models/page.dart
new file mode 100644
index 0000000..6358ef5
--- /dev/null
+++ b/lib/models/page.dart
@@ -0,0 +1,9 @@
+import 'package:flutter/material.dart';
+
+class Page {
+ Page({this.icon, this.route, this.title});
+
+ final Icon icon;
+ final String route;
+ final String title;
+}
diff --git a/lib/models/product.dart b/lib/models/product.dart
new file mode 100644
index 0000000..2198305
--- /dev/null
+++ b/lib/models/product.dart
@@ -0,0 +1,30 @@
+class Product implements Comparable {
+ Product({
+ this.description,
+ this.id,
+ this.isFavourite = false,
+ this.isInBasket = false,
+ this.isPopular = false,
+ this.name,
+ this.price,
+ this.quantity = 0,
+ this.url,
+ });
+
+ final String description;
+ final int id;
+ final bool isPopular;
+ final String name;
+ final double price;
+ final String url;
+
+ bool isFavourite;
+ bool isInBasket;
+ int quantity;
+
+ @override
+ int compareTo(Product other) => id - other.id;
+
+ bool operator ==(other) => other is Product && other.id == id;
+ int get hashCode => id.hashCode;
+}
diff --git a/lib/pages/basket.dart b/lib/pages/basket.dart
new file mode 100644
index 0000000..63cf6ed
--- /dev/null
+++ b/lib/pages/basket.dart
@@ -0,0 +1,149 @@
+import 'package:avon_farm_foods/enums/clear_basket_dialog_action.dart';
+import 'package:avon_farm_foods/models/product.dart';
+import 'package:avon_farm_foods/stores/basket.dart';
+import 'package:avon_farm_foods/widgets/dismissible_list_tile.dart';
+import 'package:avon_farm_foods/widgets/drawer.dart';
+import 'package:avon_farm_foods/widgets/product_list_tile.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_flux/flutter_flux.dart';
+import 'package:numberpicker/numberpicker.dart';
+
+class BasketPage extends StatefulWidget {
+ @override
+ _BasketPageState createState() => new _BasketPageState();
+}
+
+class _BasketPageState extends State
+ with StoreWatcherMixin {
+ BasketStore _basketStore;
+ List _products;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _basketStore = listenToStore(basketStoreToken, _handleBasketStoreChanged);
+
+ _products = _basketStore.products;
+ }
+
+ AppBar _buildAppBar() {
+ return new AppBar(
+ actions: _buildAppBarActions(),
+ title: new Text('Basket'),
+ );
+ }
+
+ List _buildAppBarActions() {
+ return [
+ new IconButton(
+ icon: new Icon(Icons.delete),
+ onPressed: _basketStore.isEmpty
+ ? null
+ : () {
+ showDialog(
+ context: context,
+ child: _buildClearBasketDialog(),
+ ).then((value) {
+ if (value == ClearBasketDialogAction.yes) clearBasket();
+ });
+ },
+ tooltip: 'Clear Basket',
+ ),
+ new IconButton(
+ icon: new Icon(Icons.payment),
+ onPressed: _basketStore.isEmpty
+ ? null
+ : () => Navigator.of(context).pushNamed('/checkout'),
+ tooltip: 'Enter Checkout',
+ ),
+ ];
+ }
+
+ AlertDialog _buildClearBasketDialog() {
+ return new AlertDialog(
+ actions: _buildClearBasketDialogActions(),
+ content: new Text('Are you sure you want to clear your basket?'),
+ title: new Text('Clear Basket'),
+ );
+ }
+
+ List _buildClearBasketDialogActions() {
+ return [
+ new ButtonTheme.bar(
+ child: new ButtonBar(
+ children: [
+ _buildDialogButton('NO', ClearBasketDialogAction.no),
+ _buildDialogButton('YES', ClearBasketDialogAction.yes),
+ ],
+ ),
+ ),
+ ];
+ }
+
+ FlatButton _buildDialogButton(String text, dynamic action) {
+ return new FlatButton(
+ child: new Text(text),
+ onPressed: () {
+ return Navigator.pop(
+ context,
+ action,
+ );
+ },
+ );
+ }
+
+ NumberPickerDialog _buildEditQuantityDialog(int quantity) {
+ return new NumberPickerDialog.integer(
+ initialIntegerValue: quantity,
+ maxValue: 99,
+ minValue: 1,
+ title: new Text('Edit Quantity'),
+ );
+ }
+
+ void _handleBasketStoreChanged(BasketStore basketStore) {
+ _products = basketStore.products;
+
+ setState(() {});
+ }
+
+ void _onQuantityPressed(Product product) {
+ showDialog(
+ child: _buildEditQuantityDialog(product.quantity),
+ context: context,
+ ).then((int quantity) {
+ product.quantity = quantity;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new Scaffold(
+ appBar: _buildAppBar(),
+ body: new ListView(
+ children: _products.map((Product product) {
+ return new DismissibleListTileWidget(
+ child: new ProductListTileWidget(
+ onQuantityPressed: () => _onQuantityPressed(product),
+ product: product,
+ ),
+ dismissedSnackBar: new SnackBar(
+ action: new SnackBarAction(
+ label: 'UNDO',
+ onPressed: () => addProductToBasket(product),
+ ),
+ content: new Text('Product Removed'),
+ ),
+ dismissibleBackgroundIcon: new Icon(Icons.delete),
+ key: new ObjectKey(product),
+ onDismissed: (DismissDirection direction) {
+ removeProductFromBasket(product);
+ },
+ );
+ }).toList(),
+ ),
+ drawer: new DrawerWidget(),
+ );
+ }
+}
diff --git a/lib/pages/checkout.dart b/lib/pages/checkout.dart
new file mode 100644
index 0000000..35af430
--- /dev/null
+++ b/lib/pages/checkout.dart
@@ -0,0 +1,213 @@
+import 'package:avon_farm_foods/models/checkout.dart';
+import 'package:avon_farm_foods/stores/basket.dart';
+import 'package:avon_farm_foods/widgets/form_section_divider.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_flux/flutter_flux.dart';
+
+class CheckoutPage extends StatefulWidget {
+ @override
+ _CheckoutPageState createState() => new _CheckoutPageState();
+}
+
+class _CheckoutPageState extends State
+ with StoreWatcherMixin {
+ BasketStore _basketStore;
+ Checkout _checkout = new Checkout();
+ bool _shouldAutoValidate = false;
+
+ final GlobalKey _formKey = new GlobalKey();
+ final EdgeInsets _formSectionPadding = new EdgeInsets.fromLTRB(
+ 16.0,
+ 0.0,
+ 16.0,
+ 16.0,
+ );
+
+ @override
+ void initState() {
+ super.initState();
+
+ _basketStore = listenToStore(basketStoreToken);
+ }
+
+ AppBar _buildAppBar() {
+ return new AppBar(
+ title: new Text('Checkout'),
+ actions: [
+ new Column(
+ children: [
+ new Container(
+ child: new Text('£${_basketStore.total.toStringAsFixed(2)}'),
+ padding: new EdgeInsets.only(right: 8.0),
+ ),
+ ],
+ mainAxisAlignment: MainAxisAlignment.center,
+ ),
+ ],
+ );
+ }
+
+ Form _buildForm() {
+ return new Form(
+ autovalidate: _shouldAutoValidate,
+ child: new SingleChildScrollView(
+ child: new Column(
+ children: [
+ new FormSectionDividerWidget(title: 'Personal Details'),
+ new Container(
+ child: new Column(
+ children: [
+ new TextFormField(
+ decoration: new InputDecoration(
+ labelText: 'First Name',
+ ),
+ onSaved: (String value) => _checkout.firstName = value,
+ validator: (String value) {
+ return value.isEmpty
+ ? 'Please enter your first name.'
+ : null;
+ },
+ ),
+ new TextFormField(
+ decoration: new InputDecoration(
+ labelText: 'Last Name',
+ ),
+ onSaved: (String value) => _checkout.lastName = value,
+ validator: (String value) {
+ return value.isEmpty
+ ? 'Please enter your last name.'
+ : null;
+ },
+ ),
+ ],
+ ),
+ padding: _formSectionPadding,
+ ),
+ new FormSectionDividerWidget(title: 'Delivery Details'),
+ new Container(
+ child: new Column(
+ children: [
+ new TextFormField(
+ decoration: new InputDecoration(
+ labelText: 'Address',
+ ),
+ onSaved: (String value) => _checkout.address = value,
+ validator: (String value) {
+ return value.isEmpty
+ ? 'Please enter your delivery address.'
+ : null;
+ },
+ ),
+ new TextFormField(
+ decoration: new InputDecoration(
+ labelText: 'Postcode',
+ ),
+ onSaved: (String value) => _checkout.postcode = value,
+ validator: (String value) {
+ return value.isEmpty
+ ? 'Please enter your delivery postcode.'
+ : null;
+ },
+ ),
+ new TextFormField(
+ decoration: new InputDecoration(
+ labelText: 'Telephone',
+ ),
+ keyboardType: TextInputType.phone,
+ onSaved: (String value) {
+ return _checkout.telephone = int.parse(value);
+ },
+ validator: (String value) {
+ return value.isEmpty
+ ? 'Please enter your contact telephone.'
+ : null;
+ },
+ ),
+ ],
+ ),
+ padding: _formSectionPadding,
+ ),
+ new FormSectionDividerWidget(title: 'Payment Details'),
+ new Container(
+ child: new Column(
+ children: [
+ new TextFormField(
+ decoration: new InputDecoration(
+ hintText: '1111222233334444',
+ labelText: 'Card Number',
+ ),
+ keyboardType: TextInputType.number,
+ onSaved: (String value) {
+ return _checkout.cardNumber = int.parse(value);
+ },
+ validator: (String value) {
+ return value.isEmpty
+ ? 'Please enter your card number.'
+ : null;
+ },
+ ),
+ new TextFormField(
+ decoration: new InputDecoration(
+ hintText: 'MM/YY',
+ labelText: 'Expiry Date',
+ ),
+ keyboardType: TextInputType.datetime,
+ onSaved: (String value) => _checkout.expiryDate = value,
+ validator: (String value) {
+ return value.isEmpty
+ ? 'Please enter your expiry date.'
+ : null;
+ },
+ ),
+ new TextFormField(
+ decoration: new InputDecoration(
+ hintText: '123',
+ labelText: 'CVV Number',
+ ),
+ keyboardType: TextInputType.number,
+ onSaved: (String value) {
+ return _checkout.cvv = int.parse(value);
+ },
+ validator: (String value) {
+ return value.isEmpty
+ ? 'Please enter your card verification value.'
+ : null;
+ },
+ ),
+ ],
+ ),
+ padding: _formSectionPadding,
+ ),
+ new Container(
+ child: new RaisedButton(
+ child: new Text('Pay Now'),
+ onPressed: _handleSubmitted,
+ ),
+ padding: _formSectionPadding,
+ ),
+ ],
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ ),
+ ),
+ key: _formKey,
+ );
+ }
+
+ void _handleSubmitted() {
+ final FormState form = _formKey.currentState;
+
+ if (!form.validate()) {
+ _shouldAutoValidate = true;
+ } else {
+ form.save();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new Scaffold(
+ appBar: _buildAppBar(),
+ body: _buildForm(),
+ );
+ }
+}
diff --git a/lib/pages/home.dart b/lib/pages/home.dart
index 3cfbc17..2ed25b5 100644
--- a/lib/pages/home.dart
+++ b/lib/pages/home.dart
@@ -1,3 +1,4 @@
+import 'package:avon_farm_foods/widgets/drawer.dart';
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
@@ -6,12 +7,69 @@ class HomePage extends StatefulWidget {
}
class _HomePageState extends State {
+ Column _buildIconColumn(IconData icon, String label) {
+ return new Column(
+ children: [
+ new CircleAvatar(
+ child: new Icon(
+ icon,
+ size: 40.0,
+ ),
+ radius: 30.0,
+ ),
+ new Container(
+ child: new Text(
+ label,
+ style: new TextStyle(
+ fontSize: 16.0,
+ ),
+ ),
+ padding: new EdgeInsets.only(top: 8.0),
+ ),
+ ],
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ );
+ }
+
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Avon Farm Foods'),
),
+ body: new ListView(
+ children: [
+ new Image.asset(
+ 'images/home.jpg',
+ fit: BoxFit.cover,
+ ),
+ new Container(
+ child: new Text(
+ 'Welcome to Avon Farm Foods',
+ style: new TextStyle(
+ fontSize: 20.0,
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ padding: new EdgeInsets.only(bottom: 32.0, top: 32.0),
+ ),
+ new Container(
+ child: new Row(
+ children: [
+ _buildIconColumn(Icons.shopping_basket, 'Shopped'),
+ _buildIconColumn(Icons.credit_card, 'Secured'),
+ _buildIconColumn(Icons.local_shipping, 'Shipped'),
+ _buildIconColumn(Icons.sentiment_very_satisfied, 'Satisfied'),
+ ],
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ ),
+ ),
+ ],
+ padding: new EdgeInsets.all(16.0),
+ ),
+ drawer: new DrawerWidget(),
);
}
}
diff --git a/lib/pages/orders.dart b/lib/pages/orders.dart
new file mode 100644
index 0000000..2950a2c
--- /dev/null
+++ b/lib/pages/orders.dart
@@ -0,0 +1,72 @@
+import 'package:avon_farm_foods/models/order.dart';
+import 'package:avon_farm_foods/models/product.dart';
+import 'package:avon_farm_foods/widgets/drawer.dart';
+import 'package:avon_farm_foods/widgets/order_list_tile.dart';
+import 'package:flutter/material.dart';
+
+class OrdersPage extends StatefulWidget {
+ @override
+ _OrdersPageState createState() => new _OrdersPageState();
+}
+
+class _OrdersPageState extends State {
+ _OrdersPageState() {
+ _orders = new List.generate(25, (int index) {
+ return new Order(
+ date: new DateTime.now().subtract(
+ new Duration(days: index * 7),
+ ),
+ delivery: new DateTime.now().subtract(
+ new Duration(days: (index * 7) - 3),
+ ),
+ id: (25 - index) * 1000,
+ products: [
+ new Product(
+ id: index + 1,
+ name: 'Product ${index + 1}',
+ price: 1.99,
+ url: 'images/home.jpg',
+ ),
+ ],
+ total: 1.99,
+ );
+ });
+ }
+
+ List _orders;
+
+ AppBar _buildAppBar() {
+ return new AppBar(
+ title: new Text('Orders'),
+ );
+ }
+
+ ListView _buildOrdersList() {
+ return new ListView.builder(
+ itemBuilder: (BuildContext context, int index) {
+ return new Container(
+ child: new OrderListTileWidget(
+ order: _orders[index],
+ ),
+ decoration: new BoxDecoration(
+ border: new Border(
+ bottom: new BorderSide(
+ color: Theme.of(context).dividerColor,
+ ),
+ ),
+ ),
+ );
+ },
+ itemCount: _orders.length,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new Scaffold(
+ appBar: _buildAppBar(),
+ body: _buildOrdersList(),
+ drawer: new DrawerWidget(),
+ );
+ }
+}
diff --git a/lib/pages/products.dart b/lib/pages/products.dart
new file mode 100644
index 0000000..8f17580
--- /dev/null
+++ b/lib/pages/products.dart
@@ -0,0 +1,191 @@
+import 'package:avon_farm_foods/models/product.dart';
+import 'package:avon_farm_foods/stores/basket.dart';
+import 'package:avon_farm_foods/types/predicate.dart';
+import 'package:avon_farm_foods/widgets/drawer.dart';
+import 'package:avon_farm_foods/widgets/predicate_tab.dart';
+import 'package:avon_farm_foods/widgets/product_card.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_flux/flutter_flux.dart';
+
+class ProductsPage extends StatefulWidget {
+ @override
+ _ProductsPageState createState() => new _ProductsPageState();
+}
+
+class _ProductsPageState extends State
+ with StoreWatcherMixin {
+ _ProductsPageState() {
+ _products = new List.generate(999, (int index) {
+ return new Product(
+ description: 'Insert product description here.',
+ id: index + 1,
+ isPopular: (index + 1) % 100 == 0,
+ name: 'Product ${index + 1}',
+ price: 1.99,
+ url: 'images/home.jpg',
+ );
+ });
+
+ _searchQueryPredicate = (Product product) {
+ if (_searchQuery.text.isEmpty) {
+ return true;
+ } else {
+ return product.name.contains(
+ new RegExp(
+ _searchQuery.text,
+ caseSensitive: false,
+ ),
+ );
+ }
+ };
+ }
+
+ BasketStore _basketStore;
+ bool _isSearching = false;
+ List _products;
+ Predicate _searchQueryPredicate;
+
+ final TextEditingController _searchQuery = new TextEditingController();
+
+ final List> _tabs = [
+ new PredicateTabWidget(
+ icon: new Icon(Icons.list),
+ predicate: (Product product) => true,
+ text: 'ALL',
+ ),
+ new PredicateTabWidget(
+ icon: new Icon(Icons.thumb_up),
+ predicate: (Product product) => product.isPopular,
+ text: 'POPULAR',
+ ),
+ new PredicateTabWidget(
+ icon: new Icon(Icons.favorite),
+ predicate: (Product product) => product.isFavourite,
+ text: 'FAVOURITES',
+ )
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+
+ _basketStore = listenToStore(basketStoreToken);
+
+ _products.forEach((Product product) {
+ if (_basketStore.contains(product)) product.isInBasket = true;
+ });
+ }
+
+ List _buildActions() {
+ return [
+ new IconButton(
+ icon: new Icon(Icons.search),
+ onPressed: _handleSearchBegin,
+ tooltip: 'Search',
+ ),
+ ];
+ }
+
+ AppBar _buildAppBar() {
+ return new AppBar(
+ actions: _buildActions(),
+ bottom: _buildTabBar(),
+ title: new Text('Products'),
+ );
+ }
+
+ AppBar _buildSearchBar() {
+ return new AppBar(
+ bottom: _buildTabBar(),
+ leading: new BackButton(),
+ title: new TextField(
+ autofocus: true,
+ controller: _searchQuery,
+ decoration: new InputDecoration(
+ hintText: 'Search by product name',
+ ),
+ ),
+ );
+ }
+
+ TabBar _buildTabBar() {
+ return new TabBar(
+ tabs: _tabs,
+ );
+ }
+
+ TabBarView _buildTabBarView() {
+ return new TabBarView(
+ children: _tabs.map((PredicateTabWidget tab) {
+ return _products
+ .where(tab.predicate)
+ .where(_searchQueryPredicate)
+ .toList();
+ }).map((List products) {
+ return new ListView.builder(
+ itemBuilder: (BuildContext context, int index) {
+ return new Container(
+ child: new ProductCardWidget(
+ onPressedMoreInfo: null,
+ onToggledFavourite: () {
+ setState(() {
+ products[index].isFavourite = !products[index].isFavourite;
+ });
+ },
+ onToggledInBasket: () {
+ setState(() {
+ if (products[index].isInBasket) {
+ removeProductFromBasket(products[index]);
+
+ products[index].quantity = 0;
+ } else {
+ products[index].quantity = 1;
+
+ addProductToBasket(products[index]);
+ }
+
+ products[index].isInBasket = !products[index].isInBasket;
+ });
+ },
+ product: products[index],
+ ),
+ padding: new EdgeInsets.symmetric(vertical: 8.0),
+ );
+ },
+ itemCount: products.length,
+ padding: new EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 8.0,
+ ),
+ );
+ }).toList(),
+ );
+ }
+
+ void _handleSearchBegin() {
+ ModalRoute.of(context).addLocalHistoryEntry(new LocalHistoryEntry(
+ onRemove: () {
+ setState(() {
+ _isSearching = false;
+ _searchQuery.clear();
+ });
+ },
+ ));
+
+ setState(() {
+ _isSearching = true;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new DefaultTabController(
+ child: new Scaffold(
+ appBar: _isSearching ? _buildSearchBar() : _buildAppBar(),
+ body: _buildTabBarView(),
+ drawer: new DrawerWidget(),
+ ),
+ length: _tabs.length,
+ );
+ }
+}
diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart
new file mode 100644
index 0000000..f90da2f
--- /dev/null
+++ b/lib/pages/settings.dart
@@ -0,0 +1,58 @@
+import 'package:avon_farm_foods/enums/theme.dart';
+import 'package:avon_farm_foods/models/configuration.dart';
+import 'package:avon_farm_foods/widgets/drawer.dart';
+import 'package:avon_farm_foods/widgets/form_section_divider.dart';
+import 'package:flutter/material.dart';
+
+class SettingsPage extends StatefulWidget {
+ SettingsPage(this.configuration, this.updater);
+
+ final Configuration configuration;
+ final ValueChanged updater;
+
+ @override
+ _SettingsPageState createState() => new _SettingsPageState();
+}
+
+class _SettingsPageState extends State {
+ AppBar _buildAppBar() {
+ return new AppBar(
+ title: new Text('Settings'),
+ );
+ }
+
+ ListView _buildSettingsList() {
+ return new ListView(
+ children: [
+ new FormSectionDividerWidget(title: 'Theme'),
+ _buildThemeRadioListTile('Dark', AppTheme.dark),
+ _buildThemeRadioListTile('Light', AppTheme.light),
+ ],
+ );
+ }
+
+ RadioListTile _buildThemeRadioListTile(
+ String title,
+ AppTheme value,
+ ) {
+ return new RadioListTile(
+ groupValue: widget.configuration.theme,
+ onChanged: _handleThemeChanged,
+ title: new Text(title),
+ value: value,
+ );
+ }
+
+ void _handleThemeChanged(AppTheme theme) {
+ widget.updater(widget.configuration.copyWith(theme: theme));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new Scaffold(
+ appBar: _buildAppBar(),
+ body: _buildSettingsList(),
+ drawer: new DrawerWidget(),
+ );
+ }
+}
diff --git a/lib/stores/basket.dart b/lib/stores/basket.dart
new file mode 100644
index 0000000..a0b678b
--- /dev/null
+++ b/lib/stores/basket.dart
@@ -0,0 +1,39 @@
+import 'package:avon_farm_foods/models/product.dart';
+import 'package:flutter_flux/flutter_flux.dart';
+
+class BasketStore extends Store {
+ BasketStore() {
+ triggerOnAction(addProductToBasket, (product) {
+ _products.add(product);
+ _products.sort();
+ });
+
+ triggerOnAction(clearBasket, (_) {
+ _products.clear();
+ });
+
+ triggerOnAction(removeProductFromBasket, (product) {
+ _products.remove(product);
+ });
+ }
+
+ final List _products = [];
+
+ bool get isEmpty => _products.isEmpty;
+ List get products => new List.unmodifiable(_products);
+ double get total {
+ return _products.map((Product product) {
+ return product.price * product.quantity;
+ }).reduce((double accumulator, double currentValue) {
+ return accumulator + currentValue;
+ });
+ }
+
+ bool contains(Product product) => _products.contains(product);
+}
+
+final StoreToken basketStoreToken = new StoreToken(new BasketStore());
+
+final Action addProductToBasket = new Action();
+final Action clearBasket = new Action();
+final Action removeProductFromBasket = new Action();
diff --git a/lib/types/predicate.dart b/lib/types/predicate.dart
new file mode 100644
index 0000000..3113eaf
--- /dev/null
+++ b/lib/types/predicate.dart
@@ -0,0 +1 @@
+typedef bool Predicate(T obj);
diff --git a/lib/widgets/dismissible_list_tile.dart b/lib/widgets/dismissible_list_tile.dart
new file mode 100644
index 0000000..14217b4
--- /dev/null
+++ b/lib/widgets/dismissible_list_tile.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class DismissibleListTileWidget extends StatefulWidget {
+ DismissibleListTileWidget({
+ @required this.child,
+ this.dismissedSnackBar,
+ @required this.dismissibleBackgroundIcon,
+ Key key,
+ @required this.onDismissed,
+ })
+ : super(key: key);
+
+ @override
+ _DismissibleListTileWidgetState createState() =>
+ new _DismissibleListTileWidgetState();
+
+ final Widget child;
+ final SnackBar dismissedSnackBar;
+ final Icon dismissibleBackgroundIcon;
+ final DismissDirectionCallback onDismissed;
+}
+
+class _DismissibleListTileWidgetState extends State {
+ Container _buildBackground() {
+ return new Container(
+ child: new ListTile(
+ trailing: widget.dismissibleBackgroundIcon,
+ ),
+ color: Theme.of(context).dividerColor,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new Dismissible(
+ background: _buildBackground(),
+ child: new Container(
+ child: widget.child,
+ decoration: new BoxDecoration(
+ border: new Border(
+ bottom: new BorderSide(
+ color: Theme.of(context).dividerColor,
+ ),
+ ),
+ color: Theme.of(context).canvasColor,
+ ),
+ ),
+ direction: DismissDirection.endToStart,
+ key: widget.key,
+ onDismissed: (DismissDirection direction) {
+ widget.onDismissed(direction);
+
+ if (widget.dismissedSnackBar != null) {
+ Scaffold.of(context).showSnackBar(widget.dismissedSnackBar);
+ }
+ },
+ );
+ }
+}
diff --git a/lib/widgets/drawer.dart b/lib/widgets/drawer.dart
new file mode 100644
index 0000000..d127883
--- /dev/null
+++ b/lib/widgets/drawer.dart
@@ -0,0 +1,47 @@
+import 'package:avon_farm_foods/models/page.dart';
+import 'package:avon_farm_foods/widgets/drawer_page.dart';
+import 'package:flutter/material.dart';
+
+class DrawerWidget extends StatelessWidget {
+ final List _pages = [
+ new Page(
+ icon: new Icon(Icons.home),
+ route: '/',
+ title: 'Home',
+ ),
+ new Page(
+ icon: new Icon(Icons.search),
+ route: '/products',
+ title: 'Products',
+ ),
+ new Page(
+ icon: new Icon(Icons.shopping_basket),
+ route: '/basket',
+ title: 'Basket',
+ ),
+ new Page(
+ icon: new Icon(Icons.receipt),
+ route: '/orders',
+ title: 'Orders',
+ ),
+ new Page(
+ icon: new Icon(Icons.settings),
+ route: '/settings',
+ title: 'Settings',
+ ),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return new Drawer(
+ child: new ListView(
+ children: _pages.map((Page page) {
+ return new DrawerPageWidget(
+ onTap: () => Navigator.of(context).pushNamed(page.route),
+ page: page,
+ );
+ }).toList(),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/drawer_page.dart b/lib/widgets/drawer_page.dart
new file mode 100644
index 0000000..a814d98
--- /dev/null
+++ b/lib/widgets/drawer_page.dart
@@ -0,0 +1,29 @@
+import 'package:avon_farm_foods/models/page.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class DrawerPageWidget extends StatefulWidget {
+ DrawerPageWidget({
+ Key key,
+ @required this.onTap,
+ @required this.page,
+ })
+ : super(key: key);
+
+ final VoidCallback onTap;
+ final Page page;
+
+ @override
+ _DrawerPageWidgetState createState() => new _DrawerPageWidgetState();
+}
+
+class _DrawerPageWidgetState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return new ListTile(
+ leading: widget.page.icon,
+ onTap: widget.onTap,
+ title: new Text(widget.page.title),
+ );
+ }
+}
diff --git a/lib/widgets/form_section_divider.dart b/lib/widgets/form_section_divider.dart
new file mode 100644
index 0000000..a6fcf74
--- /dev/null
+++ b/lib/widgets/form_section_divider.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class FormSectionDividerWidget extends StatelessWidget {
+ FormSectionDividerWidget({Key key, @required this.title}) : super(key: key);
+
+ final String title;
+
+ @override
+ Widget build(BuildContext context) {
+ return new Container(
+ child: new Container(
+ child: new Text(
+ title,
+ style: new TextStyle(
+ fontSize: 20.0,
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ padding: new EdgeInsets.symmetric(vertical: 16.0),
+ ),
+ decoration: new BoxDecoration(
+ color: Theme.of(context).dividerColor,
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/order_list_tile.dart b/lib/widgets/order_list_tile.dart
new file mode 100644
index 0000000..0191124
--- /dev/null
+++ b/lib/widgets/order_list_tile.dart
@@ -0,0 +1,25 @@
+import 'package:avon_farm_foods/models/order.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class OrderListTileWidget extends StatelessWidget {
+ OrderListTileWidget({
+ Key key,
+ @required this.order,
+ })
+ : super(key: key);
+
+ final Order order;
+
+ @override
+ Widget build(BuildContext context) {
+ return new ListTile(
+ isThreeLine: true,
+ subtitle: new Text(
+ '${order.date.toIso8601String().substring(0, 10)}\n${order.delivery.toIso8601String().substring(0, 10)}',
+ ),
+ title: new Text('${order.id}'),
+ trailing: new Text('£${order.total}'),
+ );
+ }
+}
diff --git a/lib/widgets/predicate_tab.dart b/lib/widgets/predicate_tab.dart
new file mode 100644
index 0000000..fe581a4
--- /dev/null
+++ b/lib/widgets/predicate_tab.dart
@@ -0,0 +1,15 @@
+import 'package:avon_farm_foods/types/predicate.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class PredicateTabWidget extends Tab {
+ PredicateTabWidget({
+ Widget icon,
+ Key key,
+ @required this.predicate,
+ String text,
+ })
+ : super(icon: icon, key: key, text: text);
+
+ final Predicate predicate;
+}
diff --git a/lib/widgets/product_card.dart b/lib/widgets/product_card.dart
new file mode 100644
index 0000000..2fb5f37
--- /dev/null
+++ b/lib/widgets/product_card.dart
@@ -0,0 +1,81 @@
+import 'package:avon_farm_foods/models/product.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class ProductCardWidget extends StatefulWidget {
+ ProductCardWidget({
+ Key key,
+ @required this.onPressedMoreInfo,
+ @required this.onToggledFavourite,
+ @required this.onToggledInBasket,
+ @required this.product,
+ })
+ : super(key: key);
+
+ final VoidCallback onPressedMoreInfo;
+ final VoidCallback onToggledFavourite;
+ final VoidCallback onToggledInBasket;
+ final Product product;
+
+ @override
+ _ProductCardWidgetState createState() => new _ProductCardWidgetState();
+}
+
+class _ProductCardWidgetState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return new Card(
+ child: new Column(
+ children: [
+ new Image.asset(
+ widget.product.url,
+ fit: BoxFit.cover,
+ ),
+ new ListTile(
+ leading: new CircleAvatar(
+ child: new Text(
+ '${widget.product.id.toString()}',
+ style: new TextStyle(fontSize: 14.0),
+ ),
+ ),
+ subtitle: new Text('£${widget.product.price.toStringAsFixed(2)}'),
+ title: new Text(
+ widget.product.name,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ new ButtonTheme.bar(
+ child: new ButtonBar(
+ children: [
+ new IconButton(
+ icon: new Icon(Icons.info),
+ onPressed: widget.onPressedMoreInfo,
+ ),
+ new IconButton(
+ icon: new Icon(
+ Icons.shopping_basket,
+ color: widget.product.isInBasket
+ ? Theme.of(context).textTheme.title.color
+ : Theme.of(context).disabledColor,
+ ),
+ onPressed: widget.onToggledInBasket,
+ ),
+ new IconButton(
+ icon: new Icon(
+ Icons.favorite,
+ color: widget.product.isFavourite
+ ? Colors.red
+ : Theme.of(context).disabledColor,
+ ),
+ onPressed: widget.onToggledFavourite,
+ ),
+ ],
+ ),
+ ),
+ ],
+ mainAxisSize: MainAxisSize.min,
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/product_list_tile.dart b/lib/widgets/product_list_tile.dart
new file mode 100644
index 0000000..99f970d
--- /dev/null
+++ b/lib/widgets/product_list_tile.dart
@@ -0,0 +1,33 @@
+import 'package:avon_farm_foods/models/product.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class ProductListTileWidget extends StatelessWidget {
+ ProductListTileWidget({
+ Key key,
+ @required this.onQuantityPressed,
+ @required this.product,
+ })
+ : super(key: key);
+
+ final VoidCallback onQuantityPressed;
+ final Product product;
+
+ @override
+ Widget build(BuildContext context) {
+ return new ListTile(
+ leading: new CircleAvatar(
+ child: new Text(
+ '${product.id}',
+ style: new TextStyle(fontSize: 14.0),
+ ),
+ ),
+ subtitle: new Text('£${product.price.toStringAsFixed(2)}'),
+ title: new Text(product.name),
+ trailing: new FlatButton(
+ child: new Text('${product.quantity}'),
+ onPressed: onQuantityPressed,
+ ),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 7626871..7efb0d1 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -36,6 +36,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
+ build:
+ dependency: transitive
+ description:
+ name: build
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.0+2"
+ build_config:
+ dependency: transitive
+ description:
+ name: build_config
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.5"
+ build_resolvers:
+ dependency: transitive
+ description:
+ name: build_resolvers
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.1"
+ build_runner:
+ dependency: "direct dev"
+ description:
+ name: build_runner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.7.11+1"
+ built_collection:
+ dependency: transitive
+ description:
+ name: built_collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.5"
+ built_value:
+ dependency: transitive
+ description:
+ name: built_value
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.2.1"
charcode:
dependency: transitive
description:
@@ -50,6 +92,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2+1"
+ code_builder:
+ dependency: transitive
+ description:
+ name: code_builder
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.3"
collection:
dependency: transitive
description:
@@ -85,11 +134,32 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1"
+ dart_style:
+ dependency: transitive
+ description:
+ name: dart_style
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.9+1"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.10.7"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
+ flutter_flux:
+ dependency: "direct main"
+ description:
+ path: "..\\flutter_flux"
+ relative: true
+ source: path
+ version: "4.0.1"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -109,6 +179,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.5"
+ graphs:
+ dependency: transitive
+ description:
+ name: graphs
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.0"
html:
dependency: transitive
description:
@@ -158,6 +235,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1"
+ json_annotation:
+ dependency: "direct main"
+ description:
+ name: json_annotation
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.2"
+ json_serializable:
+ dependency: "direct dev"
+ description:
+ name: json_serializable
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.2"
kernel:
dependency: transitive
description:
@@ -214,6 +305,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
+ numberpicker:
+ dependency: "direct main"
+ description:
+ name: numberpicker
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.0"
package_config:
dependency: transitive
description:
@@ -263,6 +361,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.28.0"
+ shared_preferences:
+ dependency: "direct main"
+ description:
+ name: shared_preferences
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.4.0"
shelf:
dependency: transitive
description:
@@ -296,6 +401,13 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
+ source_gen:
+ dependency: transitive
+ description:
+ name: source_gen
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.7.6"
source_map_stack_trace:
dependency: transitive
description:
@@ -331,6 +443,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.3"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.0.10"
string_scanner:
dependency: transitive
description:
@@ -395,4 +514,5 @@ packages:
source: hosted
version: "2.1.13"
sdks:
- dart: ">=2.0.0-dev.23.0 <=2.0.0-edge.0d5cf900b021bf5c9fa593ffa12b15bcd1cc5fe0"
+ dart: ">=2.0.0-dev.28.0 <=2.0.0-edge.0d5cf900b021bf5c9fa593ffa12b15bcd1cc5fe0"
+ flutter: ">=0.1.4 <2.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 14e83be..90ad6e5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -5,14 +5,21 @@ dependencies:
flutter:
sdk: flutter
- # The following adds the Cupertino Icons font to your application.
- # Use with the CupertinoIcons class for iOS style icons.
+ flutter_flux:
+ path: ../flutter_flux
+
cupertino_icons: ^0.1.0
+ json_annotation: ^0.2.0
+ numberpicker: ^0.1.0
+ shared_preferences: ^0.4.0
dev_dependencies:
flutter_test:
sdk: flutter
+ build_runner: ^0.7.0
+ json_serializable: ^0.3.0
+
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
@@ -26,9 +33,9 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
- # assets:
- # - images/a_dot_burr.jpeg
- # - images/a_dot_ham.jpeg
+ assets:
+ - images/home.jpg
+ - images/splash_screen.png
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.io/assets-and-images/#resolution-aware.