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);
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() {
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> {

@ -20,5 +20,6 @@ class FetchProductsMiddleware {
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/state.dart';
final productQuantitiesReducer = combineReducers([
final productQuantitiesReducer = combineReducers<Quantities>([
TypedReducer<Quantities, IncrementProductAction>(_increment),
TypedReducer<Quantities, DecrementProductAction>(_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;
}

@ -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<RemoteResource<List<Product>>, FetchProductsAction>(
fetchProducts),
TypedReducer<RemoteResource<List<Product>>, FetchProductsSucceededAction>(
productFetched),
TypedReducer<RemoteResource<List<Product>>, FetchProductsFailedAction>(
errorOccurred),
final productReducer = combineReducers<RemoteProducts>([
TypedReducer<RemoteProducts, FetchProductsAction>(fetchProducts),
TypedReducer<RemoteProducts, FetchProductsSucceededAction>(productFetched),
TypedReducer<RemoteProducts, FetchProductsFailedAction>(errorOccurred),
]);
RemoteResource<List<Product>> fetchProducts(
RemoteResource<List<Product>> state,
RemoteProducts fetchProducts(
RemoteProducts state,
FetchProductsAction action,
) =>
const RemoteResource.loading();
RemoteResource<List<Product>> productFetched(
RemoteResource<List<Product>> state,
FetchProductsSucceededAction action) =>
RemoteProducts productFetched(
RemoteProducts state, FetchProductsSucceededAction action) =>
RemoteResource.finished(action.products);
RemoteResource<List<Product>> errorOccurred(RemoteResource<List<Product>> state,
FetchProductsFailedAction action) =>
RemoteProducts errorOccurred(
RemoteProducts state, FetchProductsFailedAction action) =>
RemoteResource.error(action.errorMessage);

@ -1,7 +1,7 @@
import 'package:redux/redux.dart';
import 'package:thesis_shop/redux/actions/actions.dart';
final userReducer = combineReducers([
final userReducer = combineReducers<bool>([
TypedReducer<bool, SingInAction>(_signIn),
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/remote_resource.dart';
typedef RemoteProducts = RemoteResource<List<Product>>;
typedef ProductTitle = String;
typedef Quantities = Map<ProductTitle, int>;
class AppState {
final bool isSignedIn;
final RemoteResource<List<Product>> 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<CartItem> get cart {
final products = remoteProducts is FinishedRemoteResource
? remoteProducts.asFinished().value
final products = remoteProductsWithDiscount is FinishedRemoteResource
? remoteProductsWithDiscount.asFinished().value
: <Product>[];
final productByTitle = {
for (final product in products) product.title: product
@ -34,6 +41,12 @@ class AppState {
.toList();
}
double get totalPrice => cart.fold<double>(
0.0,
(previousValue, element) =>
previousValue + element.amount * element.product.price,
);
AppState copyWith({
bool? isSignedIn,
RemoteResource<List<Product>>? remoteProducts,

@ -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<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_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<AppState, String>(
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),
),
),
),
);

@ -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<AppState, int>(
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)'),
);
},
);
}
}

@ -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<AppState>(
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)),
),
),
);
}
}

@ -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<AppState, RemoteProducts>(
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_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<AppState>(
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,
)
],
);
},
);
}
}

Loading…
Cancel
Save