1
0
Fork 0

Integrate redux

redux
Jonas Franz 2 years ago
parent 2f6b322136
commit 52dcc87b1c
  1. 2
      lib/models/product.dart
  2. 11
      lib/models/remote_resource.dart
  3. 1
      lib/redux/middlewares/fetch_products_middleware.dart
  4. 8
      lib/redux/reducers/product_quantities_reducer.dart
  5. 26
      lib/redux/reducers/product_reducer.dart
  6. 2
      lib/redux/reducers/user_reducer.dart
  7. 19
      lib/redux/state.dart
  8. 20
      lib/screens/cart/cart_screen.dart
  9. 18
      lib/screens/cart/total_price_text.dart
  10. 17
      lib/screens/product_list/cart_button.dart
  11. 16
      lib/screens/product_list/product_item.dart
  12. 22
      lib/screens/product_list/product_list_screen.dart
  13. 31
      lib/widgets/user_switch.dart

@ -8,4 +8,6 @@ class Product {
String get priceAsString => price.toStringAsFixed(2); String get priceAsString => price.toStringAsFixed(2);
const Product({required this.title, required this.price}); const Product({required this.title, required this.price});
Product copyWithDiscount() => Product(title: title, price: price * 0.8);
} }

@ -19,6 +19,17 @@ abstract class RemoteResource<T> {
ErrorRemoteResource<T> asError() { ErrorRemoteResource<T> asError() {
return this as ErrorRemoteResource<T>; return this as ErrorRemoteResource<T>;
} }
R when<R>({
required R Function(T) finished,
required R Function(String errorMessage) error,
required R Function() loading,
}) {
if (this is FinishedRemoteResource<T>) return finished(asFinished().value);
if (this is ErrorRemoteResource<T>) return error(asError().errorMessage);
if (this is LoadingRemoteResource<T>) return loading();
throw UnimplementedError();
}
} }
class ErrorRemoteResource<T> extends RemoteResource<T> { class ErrorRemoteResource<T> extends RemoteResource<T> {

@ -20,5 +20,6 @@ class FetchProductsMiddleware {
FetchProductsFailedAction(errorMessage: error.toString())); FetchProductsFailedAction(errorMessage: error.toString()));
}); });
} }
next(action);
} }
} }

@ -2,7 +2,7 @@ import 'package:redux/redux.dart';
import 'package:thesis_shop/redux/actions/actions.dart'; import 'package:thesis_shop/redux/actions/actions.dart';
import 'package:thesis_shop/redux/state.dart'; import 'package:thesis_shop/redux/state.dart';
final productQuantitiesReducer = combineReducers([ final productQuantitiesReducer = combineReducers<Quantities>([
TypedReducer<Quantities, IncrementProductAction>(_increment), TypedReducer<Quantities, IncrementProductAction>(_increment),
TypedReducer<Quantities, DecrementProductAction>(_decrement), TypedReducer<Quantities, DecrementProductAction>(_decrement),
]); ]);
@ -10,7 +10,11 @@ final productQuantitiesReducer = combineReducers([
Quantities _changeAmount(Quantities state, ChangeProductAmountAction action) { Quantities _changeAmount(Quantities state, ChangeProductAmountAction action) {
final newState = Map.of(state); final newState = Map.of(state);
final currentAmount = newState.putIfAbsent(action.product.title, () => 0); 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; return newState..[action.product.title] = currentAmount + action.amount;
} }

@ -1,29 +1,25 @@
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:thesis_shop/models/product.dart';
import 'package:thesis_shop/models/remote_resource.dart'; import 'package:thesis_shop/models/remote_resource.dart';
import '../actions/actions.dart'; import '../actions/actions.dart';
import '../state.dart';
final productReducer = combineReducers([ final productReducer = combineReducers<RemoteProducts>([
TypedReducer<RemoteResource<List<Product>>, FetchProductsAction>( TypedReducer<RemoteProducts, FetchProductsAction>(fetchProducts),
fetchProducts), TypedReducer<RemoteProducts, FetchProductsSucceededAction>(productFetched),
TypedReducer<RemoteResource<List<Product>>, FetchProductsSucceededAction>( TypedReducer<RemoteProducts, FetchProductsFailedAction>(errorOccurred),
productFetched),
TypedReducer<RemoteResource<List<Product>>, FetchProductsFailedAction>(
errorOccurred),
]); ]);
RemoteResource<List<Product>> fetchProducts( RemoteProducts fetchProducts(
RemoteResource<List<Product>> state, RemoteProducts state,
FetchProductsAction action, FetchProductsAction action,
) => ) =>
const RemoteResource.loading(); const RemoteResource.loading();
RemoteResource<List<Product>> productFetched( RemoteProducts productFetched(
RemoteResource<List<Product>> state, RemoteProducts state, FetchProductsSucceededAction action) =>
FetchProductsSucceededAction action) =>
RemoteResource.finished(action.products); RemoteResource.finished(action.products);
RemoteResource<List<Product>> errorOccurred(RemoteResource<List<Product>> state, RemoteProducts errorOccurred(
FetchProductsFailedAction action) => RemoteProducts state, FetchProductsFailedAction action) =>
RemoteResource.error(action.errorMessage); RemoteResource.error(action.errorMessage);

@ -1,7 +1,7 @@
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:thesis_shop/redux/actions/actions.dart'; import 'package:thesis_shop/redux/actions/actions.dart';
final userReducer = combineReducers([ final userReducer = combineReducers<bool>([
TypedReducer<bool, SingInAction>(_signIn), TypedReducer<bool, SingInAction>(_signIn),
TypedReducer<bool, SignOutAction>(_signOut) TypedReducer<bool, SignOutAction>(_signOut)
]); ]);

@ -2,12 +2,14 @@ import 'package:thesis_shop/models/cart_item.dart';
import 'package:thesis_shop/models/product.dart'; import 'package:thesis_shop/models/product.dart';
import 'package:thesis_shop/models/remote_resource.dart'; import 'package:thesis_shop/models/remote_resource.dart';
typedef RemoteProducts = RemoteResource<List<Product>>;
typedef ProductTitle = String; typedef ProductTitle = String;
typedef Quantities = Map<ProductTitle, int>; typedef Quantities = Map<ProductTitle, int>;
class AppState { class AppState {
final bool isSignedIn; final bool isSignedIn;
final RemoteResource<List<Product>> remoteProducts; final RemoteProducts remoteProducts;
final Quantities productQuantities; final Quantities productQuantities;
const AppState({ const AppState({
@ -20,9 +22,14 @@ class AppState {
remoteProducts = const RemoteResource.loading(), remoteProducts = const RemoteResource.loading(),
productQuantities = {}; productQuantities = {};
RemoteProducts get remoteProductsWithDiscount => isSignedIn
? remoteProducts.mapIfFinished((products) =>
products.map((product) => product.copyWithDiscount()).toList())
: remoteProducts;
List<CartItem> get cart { List<CartItem> get cart {
final products = remoteProducts is FinishedRemoteResource final products = remoteProductsWithDiscount is FinishedRemoteResource
? remoteProducts.asFinished().value ? remoteProductsWithDiscount.asFinished().value
: <Product>[]; : <Product>[];
final productByTitle = { final productByTitle = {
for (final product in products) product.title: product for (final product in products) product.title: product
@ -34,6 +41,12 @@ class AppState {
.toList(); .toList();
} }
double get totalPrice => cart.fold<double>(
0.0,
(previousValue, element) =>
previousValue + element.amount * element.product.price,
);
AppState copyWith({ AppState copyWith({
bool? isSignedIn, bool? isSignedIn,
RemoteResource<List<Product>>? remoteProducts, RemoteResource<List<Product>>? remoteProducts,

@ -1,17 +1,12 @@
import 'package:flutter/material.dart'; 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/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/screens/cart/total_price_text.dart';
import 'package:thesis_shop/widgets/user_switch.dart'; import 'package:thesis_shop/widgets/user_switch.dart';
import 'cart_item_list.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 { class CartScreen extends StatelessWidget {
const CartScreen({Key? key}) : super(key: key); const CartScreen({Key? key}) : super(key: key);
@ -24,9 +19,14 @@ class CartScreen extends StatelessWidget {
), ),
body: Column( body: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: const [ children: [
Expanded(child: CartItemList(items: _placeHolderItems)), Expanded(
TotalPriceText(), child: StoreConnector<AppState, List<CartItem>>(
converter: (store) => store.state.cart,
builder: (context, cart) => CartItemList(items: cart),
),
),
const TotalPriceText(),
], ],
), ),
); );

@ -1,16 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:thesis_shop/redux/state.dart';
class TotalPriceText extends StatelessWidget { class TotalPriceText extends StatelessWidget {
const TotalPriceText({Key? key}) : super(key: key); const TotalPriceText({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return StoreConnector<AppState, String>(
child: Padding( converter: (store) => store.state.totalPrice.toStringAsFixed(2),
padding: const EdgeInsets.all(8.0), builder: (context, price) => SafeArea(
child: Text( child: Padding(
'Gesamtpreis: 27€', padding: const EdgeInsets.all(8.0),
style: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 24), child: Text(
'Gesamtpreis: $price',
style:
Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 24),
),
), ),
), ),
); );

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:thesis_shop/benchmark_counter.dart'; import 'package:thesis_shop/benchmark_counter.dart';
import 'package:thesis_shop/redux/state.dart';
import 'package:thesis_shop/route_key.dart'; import 'package:thesis_shop/route_key.dart';
class CartButton extends StatelessWidget { class CartButton extends StatelessWidget {
@ -7,11 +9,16 @@ class CartButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
BenchmarkCounters.cartButton++; return StoreConnector<AppState, int>(
return ElevatedButton.icon( converter: (store) => store.state.cart.length,
onPressed: () => Navigator.of(context).pushRouteKey(RouteKey.cart), builder: (context, itemCount) {
icon: const Icon(Icons.shopping_basket), BenchmarkCounters.cartButton++;
label: const Text('Warenkorb (3 Produkte)'), return ElevatedButton.icon(
onPressed: () => Navigator.of(context).pushRouteKey(RouteKey.cart),
icon: const Icon(Icons.shopping_basket),
label: Text('Warenkorb ($itemCount Produkte)'),
);
},
); );
} }
} }

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:thesis_shop/models/product.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'; import 'package:thesis_shop/widgets/number_picker.dart';
class ProductItem extends StatelessWidget { class ProductItem extends StatelessWidget {
@ -8,9 +11,16 @@ class ProductItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return StoreBuilder<AppState>(
title: Text('${product.title} (${product.priceAsString}€/Stück)'), builder: (context, store) => ListTile(
trailing: NumberPicker(value: 5, onUp: () {}, onDown: () {}), 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)),
),
),
); );
} }
} }

@ -1,30 +1,30 @@
import 'package:flutter/material.dart'; 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 'package:thesis_shop/widgets/user_switch.dart';
import 'cart_button_overlay.dart'; import 'cart_button_overlay.dart';
import 'product_list.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 { class ProductListScreen extends StatelessWidget {
const ProductListScreen({Key? key}) : super(key: key); const ProductListScreen({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const products = _productPlaceholder;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Thesis Shop'), title: const Text('Thesis Shop'),
actions: [UserSwitch(isOn: true, onChanged: (_) {})], actions: [UserSwitch(isOn: true, onChanged: (_) {})],
), ),
body: const CartButtonOverlay( body: StoreConnector<AppState, RemoteProducts>(
child: ProductList(products: products), 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()),
),
), ),
); );
} }

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:thesis_shop/benchmark_counter.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 { class UserSwitch extends StatelessWidget {
final bool isOn; final bool isOn;
@ -11,17 +14,23 @@ class UserSwitch extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
BenchmarkCounters.userSwitch++; return StoreBuilder<AppState>(
return Row( builder: (context, store) {
mainAxisSize: MainAxisSize.min, BenchmarkCounters.userSwitch++;
children: [ return Row(
const Icon(Icons.account_circle), mainAxisSize: MainAxisSize.min,
Switch( children: [
value: isOn, const Icon(Icons.account_circle),
onChanged: onChanged, Switch(
activeColor: Colors.green, value: store.state.isSignedIn,
) onChanged: (newState) => store.dispatch(
], newState ? SingInAction() : SignOutAction(),
),
activeColor: Colors.green,
)
],
);
},
); );
} }
} }

Loading…
Cancel
Save