feat: add interface to shopping cart

This commit is contained in:
mike doornenbal 2024-07-03 13:31:21 +02:00
parent fd8afbde03
commit 1b78b2c674
9 changed files with 159 additions and 285 deletions

View file

@ -2,6 +2,5 @@
library flutter_shopping_cart;
export "src/config/shopping_cart_config.dart";
export "src/config/shopping_cart_localizations.dart";
export "src/services/product_service.dart";
export "src/config/shopping_cart_translations.dart";
export "src/widgets/shopping_cart_screen.dart";

View file

@ -1,73 +1,48 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
import "package:flutter_shopping_cart/src/widgets/product_item_popup.dart";
Widget _defaultNoContentBuilder(BuildContext context) =>
const SizedBox.shrink();
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Shopping cart configuration
///
/// This class is used to configure the shopping cart.
class ShoppingCartConfig<T extends Product> {
class ShoppingCartConfig {
/// Creates a shopping cart configuration.
ShoppingCartConfig({
required this.productService,
required this.service,
required this.onConfirmOrder,
this.productItemBuilder = _defaultProductItemBuilder,
this.onConfirmOrder,
this.confirmOrderButtonBuilder,
this.confirmOrderButtonBuilder = _defaultConfirmOrderButton,
this.confirmOrderButtonHeight = 100,
this.sumBottomSheetBuilder,
this.sumBottomSheetBuilder = _defaultSumBottomSheetBuilder,
this.sumBottomSheetHeight = 100,
this.titleBuilder,
this.localizations = const ShoppingCartLocalizations(),
this.padding = const EdgeInsets.symmetric(horizontal: 32),
this.translations = const ShoppingCartTranslations(),
this.pagePadding = const EdgeInsets.symmetric(horizontal: 32),
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
this.appBar,
Widget Function(BuildContext context) noContentBuilder =
_defaultNoContentBuilder,
}) : assert(
confirmOrderButtonBuilder != null || onConfirmOrder != null,
"""
If you override the confirm order button builder,
you cannot use the onConfirmOrder callback.""",
),
assert(
confirmOrderButtonBuilder == null || onConfirmOrder == null,
"""
If you do not override the confirm order button builder,
you must use the onConfirmOrder callback.""",
),
_noContentBuilder = noContentBuilder;
this.appBar = _defaultAppBar,
});
/// Product Service. The service contains all the products that
/// a shopping cart can contain. Each product must extend the [Product] class.
/// The service is used to add, remove, and update products.
///
/// The service can be seperate for each shopping cart in-case you want to
/// support seperate shopping carts for shop.
ProductService<T> productService = ProductService<T>(<T>[]);
/// Product service. The product service is used to manage the products in the
/// shopping cart.
final ShoppingCartService service;
/// Product item builder. This builder is used to build the product item
/// that will be displayed in the shopping cart.
final Widget Function(
BuildContext context,
Locale locale,
Product product,
ProductService<Product> productService,
ShoppingCartConfig configuration,
) productItemBuilder;
final Widget Function(BuildContext context) _noContentBuilder;
/// No content builder. This builder is used to build the no content widget
/// that will be displayed in the shopping cart when there are no products.
Widget Function(BuildContext context) get noContentBuilder =>
_noContentBuilder;
/// Confirm order button builder. This builder is used to build the confirm
/// order button that will be displayed in the shopping cart.
/// If you override this builder, you cannot use the [onConfirmOrder] callback
final Widget Function(BuildContext context)? confirmOrderButtonBuilder;
final Widget Function(
BuildContext context,
ShoppingCartConfig configuration,
Function(List<Product> products) onConfirmOrder,
) confirmOrderButtonBuilder;
/// Confirm order button height. The height of the confirm order button.
/// This height is used to calculate the bottom padding of the shopping cart.
@ -78,12 +53,13 @@ you must use the onConfirmOrder callback.""",
/// Confirm order callback. This callback is called when the confirm order
/// button is pressed. The callback will not be called if you override the
/// confirm order button builder.
final Function(List<T> products)? onConfirmOrder;
final Function(List<Product> products) onConfirmOrder;
/// Sum bottom sheet builder. This builder is used to build the sum bottom
/// sheet that will be displayed in the shopping cart. The sum bottom sheet
/// can be used to display the total sum of the products in the shopping cart.
final Widget Function(BuildContext context)? sumBottomSheetBuilder;
final Widget Function(BuildContext context, ShoppingCartConfig configuration)
sumBottomSheetBuilder;
/// Sum bottom sheet height. The height of the sum bottom sheet.
/// This height is used to calculate the bottom padding of the shopping cart.
@ -92,31 +68,30 @@ you must use the onConfirmOrder callback.""",
/// Padding around the shopping cart. The padding is used to create space
/// around the shopping cart.
final EdgeInsets padding;
final EdgeInsets pagePadding;
/// Bottom padding of the shopping cart. The bottom padding is used to create
/// a padding around the bottom sheet. This padding is ignored when the
/// [sumBottomSheetBuilder] is overridden.
final EdgeInsets bottomPadding;
/// Title builder. This builder is used to build the title of the shopping
/// cart. The title is displayed at the top of the shopping cart. If you
/// use the title builder, the [title] will be ignored.
final Widget Function(BuildContext context)? titleBuilder;
/// Title builder. This builder is used to
/// build the title of the shopping cart.
final Widget Function(
BuildContext context,
String title,
)? titleBuilder;
/// Shopping cart localizations. The localizations are used to localize the
/// shopping cart.
final ShoppingCartLocalizations localizations;
/// Shopping cart translations. The translations for the shopping cart.
final ShoppingCartTranslations translations;
/// App bar for the shopping cart screen.
final PreferredSizeWidget? appBar;
/// Appbar for the shopping cart screen.
final AppBar Function(BuildContext context) appBar;
}
Widget _defaultProductItemBuilder(
BuildContext context,
Locale locale,
Product product,
ProductService<Product> service,
ShoppingCartConfig configuration,
) {
var theme = Theme.of(context);
@ -172,7 +147,8 @@ Widget _defaultProductItemBuilder(
Icons.remove,
color: Colors.black,
),
onPressed: () => service.removeOneProduct(product),
onPressed: () =>
configuration.service.removeOneProduct(product),
),
Padding(
padding: const EdgeInsets.all(2),
@ -198,7 +174,7 @@ Widget _defaultProductItemBuilder(
Icons.add,
color: Colors.black,
),
onPressed: () => service.addProduct(product),
onPressed: () => configuration.service.addProduct(product),
),
],
),
@ -207,3 +183,76 @@ Widget _defaultProductItemBuilder(
),
);
}
Widget _defaultSumBottomSheetBuilder(
BuildContext context,
ShoppingCartConfig configuration,
) {
var theme = Theme.of(context);
var totalPrice = configuration.service.products
.map((product) => product.price * product.quantity)
.fold(0.0, (a, b) => a + b);
return Padding(
padding: configuration.bottomPadding,
child: Row(
children: [
Text(
configuration.translations.sum,
style: theme.textTheme.titleMedium,
),
const Spacer(),
Text(
"${totalPrice.toStringAsFixed(2)}",
style: theme.textTheme.bodyMedium,
),
],
),
);
}
Widget _defaultConfirmOrderButton(
BuildContext context,
ShoppingCartConfig configuration,
Function(List<Product> products) onConfirmOrder,
) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => onConfirmOrder(
configuration.service.products,
),
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
configuration.translations.placeOrder,
style: theme.textTheme.displayLarge,
),
),
),
),
);
}
AppBar _defaultAppBar(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
title: Text(
"Shopping cart",
style: theme.textTheme.headlineLarge,
),
);
}

View file

@ -1,24 +1,14 @@
import "package:flutter/material.dart";
/// Shopping cart localizations
class ShoppingCartLocalizations {
class ShoppingCartTranslations {
/// Creates shopping cart localizations
const ShoppingCartLocalizations({
this.locale = const Locale("en", "US"),
const ShoppingCartTranslations({
this.placeOrder = "Order",
this.sum = "Subtotal:",
this.cartTitle = "Products",
this.close = "close",
});
/// Locale for the shopping cart.
/// This locale will be used to format the currency.
/// Default is English.
final Locale locale;
/// Localization for the place order button.
/// This text will only be displayed if you're not using the place order
/// button builder.
/// Text for the place order button.
final String placeOrder;
/// Localization for the sum.

View file

@ -1,71 +0,0 @@
import "package:flutter/foundation.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// Product service. This class is responsible for managing the products.
/// The service is used to add, remove, and update products.
class ProductService<T extends Product> extends ChangeNotifier {
/// Creates a product service.
ProductService(this.products);
/// List of products in the shopping cart.
final List<T> products;
/// Adds a product to the shopping cart.
void addProduct(T product) {
for (var p in products) {
if (p.id == product.id) {
p.quantity++;
notifyListeners();
return;
}
}
products.add(product);
notifyListeners();
}
/// Removes a product from the shopping cart.
void removeProduct(T product) {
for (var p in products) {
if (p.id == product.id) {
products.remove(p);
notifyListeners();
return;
}
}
notifyListeners();
}
/// Removes one product from the shopping cart.
void removeOneProduct(T product) {
for (var p in products) {
if (p.id == product.id) {
if (p.quantity > 1) {
p.quantity--;
notifyListeners();
return;
}
}
}
products.remove(product);
notifyListeners();
}
/// Counts the number of products in the shopping cart.
int countProducts() {
var count = 0;
for (var product in products) {
count += product.quantity;
}
return count;
}
/// Empties the shopping cart.
void clear() {
products.clear();
notifyListeners();
}
}

View file

@ -1,5 +1,6 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A popup that displays the product item.
class ProductItemPopup extends StatelessWidget {
@ -49,7 +50,7 @@ class ProductItemPopup extends StatelessWidget {
vertical: 8.0,
),
child: Text(
configuration.localizations.close,
configuration.translations.close,
style: theme.textTheme.displayLarge,
),
),

View file

@ -1,8 +1,8 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
/// Shopping cart screen widget.
class ShoppingCartScreen<T extends Product> extends StatelessWidget {
class ShoppingCartScreen extends StatelessWidget {
/// Creates a shopping cart screen.
const ShoppingCartScreen({
required this.configuration,
@ -10,7 +10,7 @@ class ShoppingCartScreen<T extends Product> extends StatelessWidget {
});
/// Configuration for the shopping cart screen.
final ShoppingCartConfig<T> configuration;
final ShoppingCartConfig configuration;
@override
Widget build(BuildContext context) {
@ -20,7 +20,10 @@ class ShoppingCartScreen<T extends Product> extends StatelessWidget {
child: Column(
children: [
if (configuration.titleBuilder != null) ...{
configuration.titleBuilder!(context),
configuration.titleBuilder!(
context,
configuration.translations.cartTitle,
),
} else ...{
Padding(
padding: const EdgeInsets.symmetric(
@ -29,7 +32,7 @@ class ShoppingCartScreen<T extends Product> extends StatelessWidget {
child: Row(
children: [
Text(
configuration.localizations.cartTitle,
configuration.translations.cartTitle,
style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
@ -38,22 +41,16 @@ class ShoppingCartScreen<T extends Product> extends StatelessWidget {
),
},
ListenableBuilder(
listenable: configuration.productService,
listenable: configuration.service,
builder: (context, _) {
var products = configuration.productService.products;
if (products.isEmpty) {
return configuration.noContentBuilder(context);
}
var products = configuration.service.products;
return Column(
children: [
for (var product in products)
configuration.productItemBuilder(
context,
configuration.localizations.locale,
product,
configuration.productService,
configuration,
),
// Additional whitespace at the bottom to make sure the
@ -71,24 +68,18 @@ class ShoppingCartScreen<T extends Product> extends StatelessWidget {
);
return Scaffold(
appBar: configuration.appBar ??
AppBar(
title: Text(
"Shopping cart",
style: theme.textTheme.headlineLarge,
),
),
appBar: configuration.appBar.call(context),
body: SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
Padding(
padding: configuration.padding,
padding: configuration.pagePadding,
child: productBuilder,
),
Align(
alignment: Alignment.bottomCenter,
child: _BottomSheet<T>(
child: _BottomSheet(
configuration: configuration,
),
),
@ -99,124 +90,31 @@ class ShoppingCartScreen<T extends Product> extends StatelessWidget {
}
}
class _BottomSheet<T extends Product> extends StatelessWidget {
class _BottomSheet extends StatelessWidget {
const _BottomSheet({
required this.configuration,
super.key,
});
final ShoppingCartConfig<T> configuration;
@override
Widget build(BuildContext context) {
var placeOrderButton = ListenableBuilder(
listenable: configuration.productService,
builder: (BuildContext context, Widget? child) =>
configuration.confirmOrderButtonBuilder != null
? configuration.confirmOrderButtonBuilder!(context)
: _DefaultConfirmOrderButton<T>(configuration: configuration),
);
var bottomSheet = ListenableBuilder(
listenable: configuration.productService,
builder: (BuildContext context, Widget? child) =>
configuration.sumBottomSheetBuilder != null
? configuration.sumBottomSheetBuilder!(context)
: _DefaultSumBottomSheet(configuration: configuration),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
bottomSheet,
placeOrderButton,
],
);
}
}
class _DefaultConfirmOrderButton<T extends Product> extends StatelessWidget {
const _DefaultConfirmOrderButton({
required this.configuration,
});
final ShoppingCartConfig<T> configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
void onConfirmOrderPressed(List<T> products) {
if (configuration.onConfirmOrder == null) {
return;
}
if (products.isEmpty) {
return;
}
configuration.onConfirmOrder!(products);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => onConfirmOrderPressed(
configuration.productService.products,
),
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
configuration.localizations.placeOrder,
style: theme.textTheme.displayLarge,
),
),
),
),
);
}
}
class _DefaultSumBottomSheet extends StatelessWidget {
const _DefaultSumBottomSheet({
required this.configuration,
});
final ShoppingCartConfig configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var totalPrice = configuration.productService.products
.map((product) => product.price * product.quantity)
.fold(0.0, (a, b) => a + b);
return Padding(
padding: configuration.bottomPadding,
child: Row(
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
configuration.localizations.sum,
style: theme.textTheme.titleMedium,
ListenableBuilder(
listenable: configuration.service,
builder: (BuildContext context, Widget? child) =>
configuration.sumBottomSheetBuilder(context, configuration),
),
ListenableBuilder(
listenable: configuration.service,
builder: (BuildContext context, Widget? child) =>
configuration.confirmOrderButtonBuilder(
context,
configuration,
configuration.onConfirmOrder,
),
const Spacer(),
Text(
"${totalPrice.toStringAsFixed(2)}",
style: theme.textTheme.bodyMedium,
),
],
),
);
}
}

View file

@ -1,21 +1,23 @@
name: flutter_shopping_cart
description: "A Flutter module for a shopping cart."
version: 2.0.0
publish_to: 'none'
publish_to: "none"
environment:
sdk: '>=3.3.0 <4.0.0'
sdk: ">=3.3.0 <4.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_shopping:
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping
path: packages/flutter_shopping_interface
ref: 2.0.0
dependency_overrides:
flutter_shopping_interface:
path: ../flutter_shopping_interface
dev_dependencies:
flutter_test:

View file

@ -17,4 +17,7 @@ abstract class ShoppingCartService with ChangeNotifier {
/// Clears the shopping cart.
void clear();
/// The list of products in the shopping cart.
List<Product> get products;
}

View file

@ -54,4 +54,7 @@ class LocalShoppingCartService
}
notifyListeners();
}
@override
List<Product> get products => _products;
}