mirror of
https://github.com/Iconica-Development/flutter_shopping.git
synced 2025-05-19 08:53:46 +02:00
feat: add flutter_shopping_cart package
This commit is contained in:
parent
442ee165fc
commit
1b0130e15c
20 changed files with 1169 additions and 0 deletions
49
packages/flutter_shopping_cart/.gitignore
vendored
Normal file
49
packages/flutter_shopping_cart/.gitignore
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
.metadata
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
pubspec.lock
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# env
|
||||
*dotenv
|
3
packages/flutter_shopping_cart/CHANGELOG.md
Normal file
3
packages/flutter_shopping_cart/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## 1.0.0
|
||||
|
||||
* Add initial Shopping Cart component functionality
|
195
packages/flutter_shopping_cart/CONTRIBUTING.md
Normal file
195
packages/flutter_shopping_cart/CONTRIBUTING.md
Normal file
|
@ -0,0 +1,195 @@
|
|||
# Contributing
|
||||
First off, thanks for taking the time to contribute! ❤️
|
||||
|
||||
All types of contributions are encouraged and valued.
|
||||
See the [Table of Contents](#table-of-contents) for different ways to help and details about how we handle them.
|
||||
Please make sure to read the relevant section before making your contribution.
|
||||
It will make it a lot easier for us maintainers and smooth out the experience for all involved.
|
||||
Iconica looks forward to your contributions. 🎉
|
||||
|
||||
## Table of contents
|
||||
- [Contributing](#contributing)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Code of conduct](#code-of-conduct)
|
||||
- [Legal notice](#legal-notice)
|
||||
- [I have a question](#i-have-a-question)
|
||||
- [I want to contribute](#i-want-to-contribute)
|
||||
- [Reporting bugs](#reporting-bugs)
|
||||
- [Contributing code](#contributing-code)
|
||||
|
||||
## Code of conduct
|
||||
|
||||
### Legal notice
|
||||
When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
|
||||
All accepted pull requests and other additions to this project will be considered intellectual property of Iconica.
|
||||
|
||||
All repositories should be kept clean of jokes, easter eggs and other unnecessary additions.
|
||||
|
||||
## I have a question
|
||||
|
||||
If you want to ask a question, we assume that you have read the available documentation found within the code.
|
||||
Before you ask a question, it is best to search for existing issues that might help you.
|
||||
In case you have found a suitable issue but still need clarification, you can ask your question
|
||||
It is also advisable to search the internet for answers first.
|
||||
|
||||
If you then still feel the need to ask a question and need clarification, we recommend the following:
|
||||
|
||||
- Open an issue.
|
||||
- Provide as much context as you can about what you're running into.
|
||||
|
||||
We will then take care of the issue as soon as possible.
|
||||
|
||||
## I want to contribute
|
||||
|
||||
### Reporting bugs
|
||||
|
||||
<!-- omit in toc -->
|
||||
**Before submitting a bug report**
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more information.
|
||||
Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report.
|
||||
Please complete the following steps in advance to help us fix any potential bug as fast as possible.
|
||||
|
||||
- Make sure that you are using the latest version.
|
||||
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (If you are looking for support, you might want to check [this section](#i-have-a-question)).
|
||||
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error.
|
||||
- Also make sure to search the internet (including Stack Overflow) to see if users outside of Iconica have discussed the issue.
|
||||
- Collect information about the bug:
|
||||
- Stack trace (Traceback)
|
||||
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
|
||||
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
|
||||
- Time and date of occurrence
|
||||
- Describe the expected result and actual result
|
||||
- Can you reliably reproduce the issue? And can you also reproduce it with older versions? Describe all steps that lead to the bug.
|
||||
|
||||
Once it's filed:
|
||||
|
||||
- The project team will label the issue accordingly.
|
||||
- A team member will try to reproduce the issue with your provided steps.
|
||||
If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for additional information.
|
||||
- If the team is able to reproduce the issue, it will be moved into the backlog, as well as marked with a priority, and the issue will be left to be [implemented by someone](#contributing-code).
|
||||
|
||||
### Contributing code
|
||||
|
||||
When you start working on your contribution, make sure you are aware of the relevant documentation and the functionality of the component you are working on.
|
||||
|
||||
When writing code, follow the style guidelines set by Dart: [Effective Dart](https://Dart.dev/guides/language/effective-Dart). This contains most information you will need to write clean Dart code that is well documented.
|
||||
|
||||
**Documentation**
|
||||
|
||||
As Effective Dart indicates, documenting your public methods with Dart doc comments is recommended.
|
||||
Aside from Effective Dart, we require specific information in the documentation of a method:
|
||||
|
||||
At the very least, your documentation should first name what the code does, then followed below by requirements for calling the method, the result of the method.
|
||||
Any references to internal variables or other methods should be done through [var] to indicate a reference.
|
||||
|
||||
If the method or class is complex enough (determined by the reviewers) an example is required.
|
||||
If unsure, add an example in the docs using code blocks.
|
||||
|
||||
For classes and methods, document the individual parameters with their implications.
|
||||
|
||||
> Tip: Remember that the shortest documentation can be written by having good descriptive names in the first place.
|
||||
|
||||
An example:
|
||||
```Dart
|
||||
library iconica_utilities.bidirectional_sorter;
|
||||
|
||||
part 'sorter.Dart';
|
||||
part 'enum.Dart';
|
||||
|
||||
/// Generic sort method, allow sorting of list with primitives or complex types.
|
||||
/// Uses [SortDirection] to determine the direction, either Ascending or Descending,
|
||||
/// Gets called on [List] toSort of type [T] which cannot be shorter than 2.
|
||||
/// Optionally for complex types a [Comparable] [Function] can be given to compare complex types.
|
||||
/// ```
|
||||
/// List<TestObject> objects = [];
|
||||
/// for (int i = 0; i < 10; i++) {
|
||||
/// objects.add(TestObject(name: "name", id: i));
|
||||
/// }
|
||||
///
|
||||
/// sort<TestObject>(
|
||||
/// SortDirection.descending, objects, (object) => object.id);
|
||||
///
|
||||
/// ```
|
||||
/// In the above example a list of TestObjects is created, and then sorted in descending order.
|
||||
/// If the implementation of TestObject is as following:
|
||||
/// ```
|
||||
/// class TestObject {
|
||||
/// final String name;
|
||||
/// final int id;
|
||||
///
|
||||
/// TestObject({required this.name, required this.id});
|
||||
/// }
|
||||
/// ```
|
||||
/// And the list is logged to the console, the following will appear:
|
||||
/// ```
|
||||
/// [name9, name8, name7, name6, name5, name4, name3, name2, name1, name0]
|
||||
/// ```
|
||||
|
||||
void sort<T>(
|
||||
/// Determines the sorting direction, can be either Ascending or Descending
|
||||
SortDirection sortDirection,
|
||||
|
||||
/// Incoming list, which gets sorted
|
||||
List<T> toSort, [
|
||||
|
||||
/// Optional comparable, which is only necessary for complex types
|
||||
SortFieldGetter<T>? sortValueCallback,
|
||||
]) {
|
||||
if (toSort.length < 2) return;
|
||||
assert(
|
||||
toSort.whereType<Comparable>().isNotEmpty || sortValueCallback != null);
|
||||
BidirectionalSorter<T>(
|
||||
sortInstructions: <SortInstruction<T>>[
|
||||
SortInstruction(
|
||||
sortValueCallback ?? (t) => t as Comparable, sortDirection),
|
||||
],
|
||||
).sort(toSort);
|
||||
}
|
||||
|
||||
/// same functionality as [sort] but with the added functionality
|
||||
/// of sorting multiple values
|
||||
void sortMulti<T>(
|
||||
/// Incoming list, which gets sorted
|
||||
List<T> toSort,
|
||||
|
||||
/// list of comparables to sort multiple values at once,
|
||||
/// priority based on index
|
||||
List<SortInstruction<T>> sortValueCallbacks,
|
||||
) {
|
||||
if (toSort.length < 2) return;
|
||||
assert(sortValueCallbacks.isNotEmpty);
|
||||
BidirectionalSorter<T>(
|
||||
sortInstructions: sortValueCallbacks,
|
||||
).sort(toSort);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Tests**
|
||||
|
||||
For each public method that was created, excluding widgets, which contains any form of logic (e.g. Calculations, predicates or major side-effects) tests are required.
|
||||
|
||||
A set of tests is written for each method, covering at least each path within the method. For example:
|
||||
|
||||
```Dart
|
||||
void foo() {
|
||||
try {
|
||||
var bar = doSomething();
|
||||
if (bar) {
|
||||
doSomethingElse();
|
||||
} else {
|
||||
doSomethingCool();
|
||||
}
|
||||
} catch (_) {
|
||||
displayError();
|
||||
}
|
||||
}
|
||||
```
|
||||
The method above should result in 3 tests:
|
||||
|
||||
1. A test for the path leading to displayError by the cause of an exception
|
||||
2. A test for if bar is true, resulting in doSomethingElse()
|
||||
3. A test for if bar is false, resulting in the doSomethingCool() method being called.
|
||||
|
||||
This means that we require 100% coverage of each method you test.
|
9
packages/flutter_shopping_cart/LICENSE
Normal file
9
packages/flutter_shopping_cart/LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
|||
Copyright (c) 2024 Iconica, All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
77
packages/flutter_shopping_cart/README.md
Normal file
77
packages/flutter_shopping_cart/README.md
Normal file
|
@ -0,0 +1,77 @@
|
|||
# flutter_shopping_cart
|
||||
|
||||
This component contains a shopping cart screen and the functionality for shopping carts that contain products.
|
||||
|
||||
## Features
|
||||
|
||||
* Shopping cart screen
|
||||
* Shopping cart products
|
||||
|
||||
## Usage
|
||||
|
||||
First, create your own product by extending the `Product` class:
|
||||
|
||||
```dart
|
||||
class ExampleProduct extends Product {
|
||||
ExampleProduct({
|
||||
requried super.id,
|
||||
required super.name,
|
||||
required super.price,
|
||||
required this.image,
|
||||
super.quantity,
|
||||
});
|
||||
|
||||
final String image;
|
||||
}
|
||||
```
|
||||
|
||||
Next, you can create the `ShoppingCartScreen` widget like this:
|
||||
|
||||
```dart
|
||||
var myProductService = ProductService<ExampleProduct>([]);
|
||||
|
||||
ShoppingCartScreen<ExampleProduct>(
|
||||
configuration: ShoppingCartConfig<ExampleProduct>(
|
||||
productService: myProductService,
|
||||
//
|
||||
productItemBuilder: (
|
||||
BuildContext context,
|
||||
Locale locale,
|
||||
ExampleProduct product,
|
||||
) =>
|
||||
ListTile(
|
||||
title: Text(product.name),
|
||||
subtitle: Text(product.price.toString()),
|
||||
),
|
||||
//
|
||||
onConfirmOrder: (List<ExampleProduct> products) {
|
||||
print("Placing order with products: $products");
|
||||
},
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping_cart/tree/main/example).
|
||||
|
||||
Or, you could run the example yourself:
|
||||
```
|
||||
git clone https://github.com/Iconica-Development/flutter_shopping_cart.git
|
||||
|
||||
cd flutter_shopping_cart
|
||||
|
||||
cd example
|
||||
|
||||
flutter run
|
||||
```
|
||||
|
||||
## Issues
|
||||
|
||||
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping_cart) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
|
||||
|
||||
## Want to contribute
|
||||
|
||||
If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_shopping_cart/pulls).
|
||||
|
||||
## Author
|
||||
|
||||
This flutter_shopping_cart for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
|
9
packages/flutter_shopping_cart/analysis_options.yaml
Normal file
9
packages/flutter_shopping_cart/analysis_options.yaml
Normal file
|
@ -0,0 +1,9 @@
|
|||
include: package:flutter_iconica_analysis/components_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
53
packages/flutter_shopping_cart/example/.gitignore
vendored
Normal file
53
packages/flutter_shopping_cart/example/.gitignore
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
.metadata
|
||||
pubspec.lock
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Platforms
|
||||
/android/
|
||||
/ios/
|
||||
/linux/
|
||||
/macos/
|
||||
/web/
|
||||
/windows/
|
16
packages/flutter_shopping_cart/example/README.md
Normal file
16
packages/flutter_shopping_cart/example/README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# example
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
|
@ -0,0 +1,9 @@
|
|||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
|
@ -0,0 +1,125 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
import "package:flutter_shopping_cart_example/models/example_product.dart";
|
||||
|
||||
ShoppingCartConfig<ExampleProduct> getShoppingCartConfiguration(
|
||||
BuildContext ctx,
|
||||
ProductService<ExampleProduct> productService,
|
||||
) =>
|
||||
ShoppingCartConfig<ExampleProduct>(
|
||||
// (REQUIRED) product service instance:
|
||||
productService: productService,
|
||||
|
||||
// (REQUIRED) product item builder:
|
||||
productItemBuilder: (
|
||||
BuildContext context,
|
||||
Locale locale,
|
||||
ExampleProduct product,
|
||||
) =>
|
||||
ListTile(
|
||||
title: Text(product.name),
|
||||
subtitle: Text(product.price.toStringAsFixed(2)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () => productService.removeOneProduct(product),
|
||||
),
|
||||
Text("${product.quantity}"),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => productService.addProduct(product),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// (OPTIONAL/REQUIRED) on confirm order callback:
|
||||
// Either use this callback or the placeOrderButtonBuilder.
|
||||
onConfirmOrder: (List<ExampleProduct> products) {
|
||||
if (kDebugMode) {
|
||||
print("Placing order with products: $products");
|
||||
}
|
||||
},
|
||||
|
||||
// (OPTIONAL/REQUIRED) place order button height:
|
||||
// Either use this or the onConfirmOrder callback.
|
||||
// confirmOrderButtonBuilder: (context) => ElevatedButton(
|
||||
// onPressed: () {
|
||||
// print("meow!");
|
||||
// },
|
||||
// child: Text("Place Order"),
|
||||
// ),
|
||||
|
||||
// (OPTIONAL) (RECOMMENDED) localizations:
|
||||
localizations: ShoppingCartLocalizations(
|
||||
placeOrder: "BESTEL",
|
||||
sum: "Te betalen",
|
||||
locale: Localizations.localeOf(ctx),
|
||||
),
|
||||
|
||||
// (OPTIONAL) title above product list:
|
||||
title: "Producten",
|
||||
|
||||
// (OPTIONAL) custom title builder:
|
||||
// titleBuilder: (context) => Text("Products"),
|
||||
|
||||
// (OPTIONAL) padding around the shopping cart:
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
|
||||
// (OPTIONAL) bottom padding of the shopping cart:
|
||||
bottomPadding: const EdgeInsets.fromLTRB(44, 0, 44, 32),
|
||||
|
||||
// (OPTIONAL) sum bottom sheet builder:
|
||||
// sumBottomSheetBuilder: (context) => Container(...),
|
||||
|
||||
/// (OPTIONAL) no content builder for when there are no products
|
||||
/// in the shopping cart.
|
||||
noContentBuilder: (context) => const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 128),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning,
|
||||
),
|
||||
SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
"Geen producten in winkelmandje",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// (OPTIONAL) custom appbar:
|
||||
appBar: AppBar(
|
||||
title: const Text("Shopping Cart"),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: productService.clear,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
var random = Random();
|
||||
productService.addProduct(
|
||||
ExampleProduct(
|
||||
name: "Example Product",
|
||||
price: 100,
|
||||
image: "https://via.placeholder.com/150",
|
||||
id: "example_product_id${random.nextInt(100000)}",
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
48
packages/flutter_shopping_cart/example/lib/main.dart
Normal file
48
packages/flutter_shopping_cart/example/lib/main.dart
Normal file
|
@ -0,0 +1,48 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_localizations/flutter_localizations.dart";
|
||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
import "package:flutter_shopping_cart_example/configuration/shopping_cart_configuration.dart";
|
||||
import "package:flutter_shopping_cart_example/models/example_product.dart";
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
useMaterial3: true,
|
||||
),
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale("nl", "NL"),
|
||||
],
|
||||
home: const Scaffold(
|
||||
body: _ShoppingCartScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ShoppingCartScreen extends StatelessWidget {
|
||||
const _ShoppingCartScreen();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var productService = ProductService<ExampleProduct>([]);
|
||||
|
||||
return ShoppingCartScreen<ExampleProduct>(
|
||||
configuration: getShoppingCartConfiguration(
|
||||
context,
|
||||
productService,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
|
||||
class ExampleProduct extends ShoppingCartProduct {
|
||||
ExampleProduct({
|
||||
required super.name,
|
||||
required super.price,
|
||||
required this.image,
|
||||
super.quantity,
|
||||
super.id = "example_product_id",
|
||||
});
|
||||
|
||||
final String image;
|
||||
}
|
34
packages/flutter_shopping_cart/example/pubspec.yaml
Normal file
34
packages/flutter_shopping_cart/example/pubspec.yaml
Normal file
|
@ -0,0 +1,34 @@
|
|||
name: flutter_shopping_cart_example
|
||||
description: "Demonstrates how to use the flutter_shopping_cart package."
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
flutter_shopping_cart:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_iconica_analysis:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||
ref: 7.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
|
@ -0,0 +1,8 @@
|
|||
/// Flutter component for shopping cart.
|
||||
library flutter_shopping_cart;
|
||||
|
||||
export "src/config/shopping_cart_config.dart";
|
||||
export "src/config/shopping_cart_localizations.dart";
|
||||
export "src/models/shopping_cart_product.dart";
|
||||
export "src/services/product_service.dart";
|
||||
export "src/widgets/shopping_cart_screen.dart";
|
|
@ -0,0 +1,133 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
|
||||
Widget _defaultNoContentBuilder(BuildContext context) =>
|
||||
const SizedBox.shrink();
|
||||
|
||||
/// Shopping cart configuration
|
||||
///
|
||||
/// This class is used to configure the shopping cart.
|
||||
class ShoppingCartConfig<T extends ShoppingCartProduct> {
|
||||
/// Creates a shopping cart configuration.
|
||||
ShoppingCartConfig({
|
||||
required this.productService,
|
||||
//
|
||||
this.onConfirmOrder,
|
||||
this.confirmOrderButtonBuilder,
|
||||
this.confirmOrderButtonHeight = 100,
|
||||
//
|
||||
this.sumBottomSheetBuilder,
|
||||
this.sumBottomSheetHeight = 100,
|
||||
//
|
||||
this.title,
|
||||
this.titleBuilder,
|
||||
//
|
||||
this.localizations = const ShoppingCartLocalizations(),
|
||||
//
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 32),
|
||||
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
|
||||
//
|
||||
this.appBar,
|
||||
//
|
||||
Widget Function(BuildContext context, Locale locale, T product)?
|
||||
productItemBuilder,
|
||||
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 {
|
||||
_productItemBuilder = productItemBuilder;
|
||||
_productItemBuilder ??= (context, locale, product) => ListTile(
|
||||
title: Text(product.name),
|
||||
subtitle: Text(product.price.toString()),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => productService.removeProduct(product),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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>[]);
|
||||
|
||||
late final Widget Function(BuildContext context, Locale locale, T product)?
|
||||
_productItemBuilder;
|
||||
|
||||
/// Product item builder. This builder is used to build the product item
|
||||
/// that will be displayed in the shopping cart.
|
||||
Widget Function(BuildContext context, Locale locale, T product)
|
||||
get productItemBuilder => _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;
|
||||
|
||||
/// Confirm order button height. The height of the confirm order button.
|
||||
/// This height is used to calculate the bottom padding of the shopping cart.
|
||||
/// If you override the confirm order button builder, you must provide a
|
||||
/// height.
|
||||
final double confirmOrderButtonHeight;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Sum bottom sheet height. The height of the sum bottom sheet.
|
||||
/// This height is used to calculate the bottom padding of the shopping cart.
|
||||
/// If you override the sum bottom sheet builder, you must provide a height.
|
||||
final double sumBottomSheetHeight;
|
||||
|
||||
/// Padding around the shopping cart. The padding is used to create space
|
||||
/// around the shopping cart.
|
||||
final EdgeInsets padding;
|
||||
|
||||
/// 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 of the shopping cart. The title is displayed at the top of the
|
||||
/// shopping cart. If you provide a title builder, the title will be ignored.
|
||||
final String? title;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Shopping cart localizations. The localizations are used to localize the
|
||||
/// shopping cart.
|
||||
final ShoppingCartLocalizations localizations;
|
||||
|
||||
/// App bar for the shopping cart screen.
|
||||
final PreferredSizeWidget? appBar;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
/// Shopping cart localizations
|
||||
class ShoppingCartLocalizations {
|
||||
/// Creates shopping cart localizations
|
||||
const ShoppingCartLocalizations({
|
||||
this.locale = const Locale("en", "US"),
|
||||
this.placeOrder = "PLACE ORDER",
|
||||
this.sum = "Total:",
|
||||
});
|
||||
|
||||
/// 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.
|
||||
final String placeOrder;
|
||||
|
||||
/// Localization for the sum.
|
||||
final String sum;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/// Abstract class for Product
|
||||
///
|
||||
/// All products that want to be added to the shopping cart
|
||||
/// must extend this class.
|
||||
abstract class ShoppingCartProduct {
|
||||
/// Creates a new product.
|
||||
ShoppingCartProduct({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.price,
|
||||
this.quantity = 1,
|
||||
});
|
||||
|
||||
/// Unique product identifier.
|
||||
/// This identifier will be used to identify the product in the shopping cart.
|
||||
/// If you don't provide an identifier, a random identifier will be generated.
|
||||
final String id;
|
||||
|
||||
/// Product name.
|
||||
/// This name will be displayed in the shopping cart.
|
||||
final String name;
|
||||
|
||||
/// Product price.
|
||||
/// This price will be displayed in the shopping cart.
|
||||
final double price;
|
||||
|
||||
/// Quantity for the product.
|
||||
int quantity;
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter_shopping_cart/flutter_shopping_cart.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 ShoppingCartProduct> 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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
|
||||
/// Shopping cart screen widget.
|
||||
class ShoppingCartScreen<T extends ShoppingCartProduct>
|
||||
extends StatelessWidget {
|
||||
/// Creates a shopping cart screen.
|
||||
const ShoppingCartScreen({
|
||||
required this.configuration,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Configuration for the shopping cart screen.
|
||||
final ShoppingCartConfig<T> configuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
var productBuilder = SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (configuration.titleBuilder != null) ...{
|
||||
configuration.titleBuilder!(context),
|
||||
} else if (configuration.title != null) ...{
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: Text(
|
||||
configuration.title!,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
},
|
||||
ListenableBuilder(
|
||||
listenable: configuration.productService,
|
||||
builder: (context, _) {
|
||||
var products = configuration.productService.products;
|
||||
|
||||
if (products.isEmpty) {
|
||||
return configuration.noContentBuilder(context);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
for (var product in products)
|
||||
configuration.productItemBuilder(
|
||||
context,
|
||||
configuration.localizations.locale,
|
||||
product,
|
||||
),
|
||||
// Additional whitespace at the bottom to make sure the
|
||||
// last product(s) are not hidden by the bottom sheet.
|
||||
SizedBox(
|
||||
height: configuration.confirmOrderButtonHeight +
|
||||
configuration.sumBottomSheetHeight,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
var bottomHeight = configuration.confirmOrderButtonHeight +
|
||||
configuration.sumBottomSheetHeight;
|
||||
|
||||
var bottomBlur = Container(
|
||||
height: bottomHeight,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
theme.colorScheme.surface.withOpacity(0),
|
||||
theme.colorScheme.surface.withOpacity(.5),
|
||||
theme.colorScheme.surface.withOpacity(.8),
|
||||
theme.colorScheme.surface.withOpacity(.8),
|
||||
theme.colorScheme.surface.withOpacity(.8),
|
||||
theme.colorScheme.surface.withOpacity(.8),
|
||||
theme.colorScheme.surface.withOpacity(1),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: configuration.appBar,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Padding(
|
||||
padding: configuration.padding,
|
||||
child: productBuilder,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: bottomBlur,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: _BottomSheet<T>(
|
||||
configuration: configuration,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomSheet<T extends ShoppingCartProduct> 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 ShoppingCartProduct>
|
||||
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 SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 80),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
),
|
||||
onPressed: () => onConfirmOrderPressed(
|
||||
configuration.productService.products,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
"""${configuration.localizations.placeOrder} (${configuration.productService.countProducts()})""",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
children: [
|
||||
Text(
|
||||
configuration.localizations.sum,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
totalPrice.toStringAsFixed(2),
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
32
packages/flutter_shopping_cart/pubspec.yaml
Normal file
32
packages/flutter_shopping_cart/pubspec.yaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
name: flutter_shopping_cart
|
||||
description: "A Flutter module for a shopping cart."
|
||||
version: 0.0.1
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_iconica_analysis:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||
ref: 7.0.0
|
||||
|
||||
flutter:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
Loading…
Reference in a new issue