From 52dcc87b1cfa5034c7272530c79d8b0ac6ccc887 Mon Sep 17 00:00:00 2001 From: Jonas Franz Date: Thu, 24 Feb 2022 22:57:29 +0100 Subject: [PATCH] Integrate redux --- lib/models/product.dart | 2 ++ lib/models/remote_resource.dart | 11 +++++++ .../fetch_products_middleware.dart | 1 + .../reducers/product_quantities_reducer.dart | 8 +++-- lib/redux/reducers/product_reducer.dart | 26 +++++++--------- lib/redux/reducers/user_reducer.dart | 2 +- lib/redux/state.dart | 19 ++++++++++-- lib/screens/cart/cart_screen.dart | 20 ++++++------ lib/screens/cart/total_price_text.dart | 18 +++++++---- lib/screens/product_list/cart_button.dart | 17 +++++++--- lib/screens/product_list/product_item.dart | 16 ++++++++-- .../product_list/product_list_screen.dart | 22 ++++++------- lib/widgets/user_switch.dart | 31 ++++++++++++------- 13 files changed, 126 insertions(+), 67 deletions(-) diff --git a/lib/models/product.dart b/lib/models/product.dart index 1b73556..e35f35f 100644 --- a/lib/models/product.dart +++ b/lib/models/product.dart @@ -8,4 +8,6 @@ class Product { String get priceAsString => price.toStringAsFixed(2); const Product({required this.title, required this.price}); + + Product copyWithDiscount() => Product(title: title, price: price * 0.8); } diff --git a/lib/models/remote_resource.dart b/lib/models/remote_resource.dart index 08bf284..716dd5d 100644 --- a/lib/models/remote_resource.dart +++ b/lib/models/remote_resource.dart @@ -19,6 +19,17 @@ abstract class RemoteResource { ErrorRemoteResource asError() { return this as ErrorRemoteResource; } + + R when({ + required R Function(T) finished, + required R Function(String errorMessage) error, + required R Function() loading, + }) { + if (this is FinishedRemoteResource) return finished(asFinished().value); + if (this is ErrorRemoteResource) return error(asError().errorMessage); + if (this is LoadingRemoteResource) return loading(); + throw UnimplementedError(); + } } class ErrorRemoteResource extends RemoteResource { diff --git a/lib/redux/middlewares/fetch_products_middleware.dart b/lib/redux/middlewares/fetch_products_middleware.dart index 7150b7a..bed4b08 100644 --- a/lib/redux/middlewares/fetch_products_middleware.dart +++ b/lib/redux/middlewares/fetch_products_middleware.dart @@ -20,5 +20,6 @@ class FetchProductsMiddleware { FetchProductsFailedAction(errorMessage: error.toString())); }); } + next(action); } } diff --git a/lib/redux/reducers/product_quantities_reducer.dart b/lib/redux/reducers/product_quantities_reducer.dart index 3e0b49b..7b12b8f 100644 --- a/lib/redux/reducers/product_quantities_reducer.dart +++ b/lib/redux/reducers/product_quantities_reducer.dart @@ -2,7 +2,7 @@ import 'package:redux/redux.dart'; import 'package:thesis_shop/redux/actions/actions.dart'; import 'package:thesis_shop/redux/state.dart'; -final productQuantitiesReducer = combineReducers([ +final productQuantitiesReducer = combineReducers([ TypedReducer(_increment), TypedReducer(_decrement), ]); @@ -10,7 +10,11 @@ final productQuantitiesReducer = combineReducers([ Quantities _changeAmount(Quantities state, ChangeProductAmountAction action) { final newState = Map.of(state); final currentAmount = newState.putIfAbsent(action.product.title, () => 0); - if (currentAmount + action.amount <= 0) return state; + if (currentAmount + action.amount < 0) return state; + if (currentAmount + action.amount == 0) { + newState.remove(action.product.title); + return newState; + } return newState..[action.product.title] = currentAmount + action.amount; } diff --git a/lib/redux/reducers/product_reducer.dart b/lib/redux/reducers/product_reducer.dart index 3c3dc69..686880b 100644 --- a/lib/redux/reducers/product_reducer.dart +++ b/lib/redux/reducers/product_reducer.dart @@ -1,29 +1,25 @@ import 'package:redux/redux.dart'; -import 'package:thesis_shop/models/product.dart'; import 'package:thesis_shop/models/remote_resource.dart'; import '../actions/actions.dart'; +import '../state.dart'; -final productReducer = combineReducers([ - TypedReducer>, FetchProductsAction>( - fetchProducts), - TypedReducer>, FetchProductsSucceededAction>( - productFetched), - TypedReducer>, FetchProductsFailedAction>( - errorOccurred), +final productReducer = combineReducers([ + TypedReducer(fetchProducts), + TypedReducer(productFetched), + TypedReducer(errorOccurred), ]); -RemoteResource> fetchProducts( - RemoteResource> state, +RemoteProducts fetchProducts( + RemoteProducts state, FetchProductsAction action, ) => const RemoteResource.loading(); -RemoteResource> productFetched( - RemoteResource> state, - FetchProductsSucceededAction action) => +RemoteProducts productFetched( + RemoteProducts state, FetchProductsSucceededAction action) => RemoteResource.finished(action.products); -RemoteResource> errorOccurred(RemoteResource> state, - FetchProductsFailedAction action) => +RemoteProducts errorOccurred( + RemoteProducts state, FetchProductsFailedAction action) => RemoteResource.error(action.errorMessage); diff --git a/lib/redux/reducers/user_reducer.dart b/lib/redux/reducers/user_reducer.dart index b9a6cce..0708866 100644 --- a/lib/redux/reducers/user_reducer.dart +++ b/lib/redux/reducers/user_reducer.dart @@ -1,7 +1,7 @@ import 'package:redux/redux.dart'; import 'package:thesis_shop/redux/actions/actions.dart'; -final userReducer = combineReducers([ +final userReducer = combineReducers([ TypedReducer(_signIn), TypedReducer(_signOut) ]); diff --git a/lib/redux/state.dart b/lib/redux/state.dart index 25c620e..0f8b614 100644 --- a/lib/redux/state.dart +++ b/lib/redux/state.dart @@ -2,12 +2,14 @@ import 'package:thesis_shop/models/cart_item.dart'; import 'package:thesis_shop/models/product.dart'; import 'package:thesis_shop/models/remote_resource.dart'; +typedef RemoteProducts = RemoteResource>; + typedef ProductTitle = String; typedef Quantities = Map; class AppState { final bool isSignedIn; - final RemoteResource> remoteProducts; + final RemoteProducts remoteProducts; final Quantities productQuantities; const AppState({ @@ -20,9 +22,14 @@ class AppState { remoteProducts = const RemoteResource.loading(), productQuantities = {}; + RemoteProducts get remoteProductsWithDiscount => isSignedIn + ? remoteProducts.mapIfFinished((products) => + products.map((product) => product.copyWithDiscount()).toList()) + : remoteProducts; + List get cart { - final products = remoteProducts is FinishedRemoteResource - ? remoteProducts.asFinished().value + final products = remoteProductsWithDiscount is FinishedRemoteResource + ? remoteProductsWithDiscount.asFinished().value : []; final productByTitle = { for (final product in products) product.title: product @@ -34,6 +41,12 @@ class AppState { .toList(); } + double get totalPrice => cart.fold( + 0.0, + (previousValue, element) => + previousValue + element.amount * element.product.price, + ); + AppState copyWith({ bool? isSignedIn, RemoteResource>? remoteProducts, diff --git a/lib/screens/cart/cart_screen.dart b/lib/screens/cart/cart_screen.dart index 08e9e45..c5226f5 100644 --- a/lib/screens/cart/cart_screen.dart +++ b/lib/screens/cart/cart_screen.dart @@ -1,17 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; import 'package:thesis_shop/models/cart_item.dart'; -import 'package:thesis_shop/models/product.dart'; +import 'package:thesis_shop/redux/state.dart'; import 'package:thesis_shop/screens/cart/total_price_text.dart'; import 'package:thesis_shop/widgets/user_switch.dart'; import 'cart_item_list.dart'; -const _placeHolderItems = [ - CartItem(product: Product(title: 'Äpfel', price: 3), amount: 3), - CartItem(product: Product(title: 'Äpfel', price: 3), amount: 3), - CartItem(product: Product(title: 'Äpfel', price: 3), amount: 3), -]; - class CartScreen extends StatelessWidget { const CartScreen({Key? key}) : super(key: key); @@ -24,9 +19,14 @@ class CartScreen extends StatelessWidget { ), body: Column( mainAxisSize: MainAxisSize.max, - children: const [ - Expanded(child: CartItemList(items: _placeHolderItems)), - TotalPriceText(), + children: [ + Expanded( + child: StoreConnector>( + converter: (store) => store.state.cart, + builder: (context, cart) => CartItemList(items: cart), + ), + ), + const TotalPriceText(), ], ), ); diff --git a/lib/screens/cart/total_price_text.dart b/lib/screens/cart/total_price_text.dart index a08b1e6..15d94f6 100644 --- a/lib/screens/cart/total_price_text.dart +++ b/lib/screens/cart/total_price_text.dart @@ -1,16 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:thesis_shop/redux/state.dart'; class TotalPriceText extends StatelessWidget { const TotalPriceText({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'Gesamtpreis: 27€', - style: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 24), + return StoreConnector( + converter: (store) => store.state.totalPrice.toStringAsFixed(2), + builder: (context, price) => SafeArea( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Gesamtpreis: $price€', + style: + Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 24), + ), ), ), ); diff --git a/lib/screens/product_list/cart_button.dart b/lib/screens/product_list/cart_button.dart index 5256e5e..7c4ec97 100644 --- a/lib/screens/product_list/cart_button.dart +++ b/lib/screens/product_list/cart_button.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; import 'package:thesis_shop/benchmark_counter.dart'; +import 'package:thesis_shop/redux/state.dart'; import 'package:thesis_shop/route_key.dart'; class CartButton extends StatelessWidget { @@ -7,11 +9,16 @@ class CartButton extends StatelessWidget { @override Widget build(BuildContext context) { - BenchmarkCounters.cartButton++; - return ElevatedButton.icon( - onPressed: () => Navigator.of(context).pushRouteKey(RouteKey.cart), - icon: const Icon(Icons.shopping_basket), - label: const Text('Warenkorb (3 Produkte)'), + return StoreConnector( + converter: (store) => store.state.cart.length, + builder: (context, itemCount) { + BenchmarkCounters.cartButton++; + return ElevatedButton.icon( + onPressed: () => Navigator.of(context).pushRouteKey(RouteKey.cart), + icon: const Icon(Icons.shopping_basket), + label: Text('Warenkorb ($itemCount Produkte)'), + ); + }, ); } } diff --git a/lib/screens/product_list/product_item.dart b/lib/screens/product_list/product_item.dart index 62e813d..d9060a9 100644 --- a/lib/screens/product_list/product_item.dart +++ b/lib/screens/product_list/product_item.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; import 'package:thesis_shop/models/product.dart'; +import 'package:thesis_shop/redux/actions/actions.dart'; +import 'package:thesis_shop/redux/state.dart'; import 'package:thesis_shop/widgets/number_picker.dart'; class ProductItem extends StatelessWidget { @@ -8,9 +11,16 @@ class ProductItem extends StatelessWidget { @override Widget build(BuildContext context) { - return ListTile( - title: Text('${product.title} (${product.priceAsString}€/Stück)'), - trailing: NumberPicker(value: 5, onUp: () {}, onDown: () {}), + return StoreBuilder( + builder: (context, store) => ListTile( + title: Text('${product.title} (${product.priceAsString}€/Stück)'), + trailing: NumberPicker( + value: store.state.productQuantities[product.title] ?? 0, + onUp: () => store.dispatch(IncrementProductAction(product: product)), + onDown: () => + store.dispatch(DecrementProductAction(product: product)), + ), + ), ); } } diff --git a/lib/screens/product_list/product_list_screen.dart b/lib/screens/product_list/product_list_screen.dart index 30ce3a8..e2bb217 100644 --- a/lib/screens/product_list/product_list_screen.dart +++ b/lib/screens/product_list/product_list_screen.dart @@ -1,30 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:thesis_shop/models/product.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:thesis_shop/redux/state.dart'; import 'package:thesis_shop/widgets/user_switch.dart'; import 'cart_button_overlay.dart'; import 'product_list.dart'; -const _productPlaceholder = [ - Product(title: 'Bananen', price: 3), - Product(title: 'Äpfel', price: 2), - Product(title: 'Birnen', price: 2.5), - Product(title: 'Kirschen', price: 1.2), -]; - class ProductListScreen extends StatelessWidget { const ProductListScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - const products = _productPlaceholder; return Scaffold( appBar: AppBar( title: const Text('Thesis Shop'), actions: [UserSwitch(isOn: true, onChanged: (_) {})], ), - body: const CartButtonOverlay( - child: ProductList(products: products), + body: StoreConnector( + converter: (store) => store.state.remoteProductsWithDiscount, + builder: (context, remoteProducts) => remoteProducts.when( + finished: (products) => CartButtonOverlay( + child: ProductList(products: products), + ), + error: (errorMessage) => Center(child: Text(errorMessage)), + loading: () => const Center(child: CircularProgressIndicator()), + ), ), ); } diff --git a/lib/widgets/user_switch.dart b/lib/widgets/user_switch.dart index 63107e7..b304542 100644 --- a/lib/widgets/user_switch.dart +++ b/lib/widgets/user_switch.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; import 'package:thesis_shop/benchmark_counter.dart'; +import 'package:thesis_shop/redux/actions/actions.dart'; +import 'package:thesis_shop/redux/state.dart'; class UserSwitch extends StatelessWidget { final bool isOn; @@ -11,17 +14,23 @@ class UserSwitch extends StatelessWidget { @override Widget build(BuildContext context) { - BenchmarkCounters.userSwitch++; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.account_circle), - Switch( - value: isOn, - onChanged: onChanged, - activeColor: Colors.green, - ) - ], + return StoreBuilder( + builder: (context, store) { + BenchmarkCounters.userSwitch++; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.account_circle), + Switch( + value: store.state.isSignedIn, + onChanged: (newState) => store.dispatch( + newState ? SingInAction() : SignOutAction(), + ), + activeColor: Colors.green, + ) + ], + ); + }, ); } }