feat: remove listenablebuilder from product page

This commit is contained in:
mike doornenbal 2024-07-09 15:50:25 +02:00
parent 2bf42c4acb
commit 0e7e2ff0e5
8 changed files with 326 additions and 220 deletions

View file

@ -1,7 +1,6 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Default next button for the order details page.
class DefaultNextButton extends StatelessWidget {
/// Constructor for the default next button for the order details page.
@ -15,10 +14,13 @@ class DefaultNextButton extends StatelessWidget {
/// Configuration for the order details page.
final OrderDetailConfiguration configuration;
/// Controller for the form.
final FlutterFormController controller;
/// Current step in the form.
final int currentStep;
/// Whether the form is checking pages.
final bool checkingPages;
@override

View file

@ -1,3 +1,4 @@
import "package:collection/collection.dart";
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/services/category_service.dart";
@ -5,41 +6,44 @@ import "package:flutter_product_page/src/widgets/defaults/default_appbar.dart";
import "package:flutter_product_page/src/widgets/defaults/default_error.dart";
import "package:flutter_product_page/src/widgets/defaults/default_no_content.dart";
import "package:flutter_product_page/src/widgets/defaults/default_shopping_cart_button.dart";
import "package:flutter_product_page/src/widgets/defaults/selected_categories.dart";
import "package:flutter_product_page/src/widgets/shop_selector.dart";
import "package:flutter_product_page/src/widgets/weekly_discount.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A page that displays products.
class ProductPageScreen extends StatelessWidget {
class ProductPageScreen extends StatefulWidget {
/// Constructor for the product page.
const ProductPageScreen({
required this.configuration,
this.initialBuildShopId,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
/// An optional initial shop ID to select. This overrides the initialShopId
/// from the configuration.
final String? initialBuildShopId;
@override
State<ProductPageScreen> createState() => _ProductPageScreenState();
}
class _ProductPageScreenState extends State<ProductPageScreen> {
@override
Widget build(BuildContext context) => Scaffold(
appBar: configuration.appBarBuilder?.call(context) ??
appBar: widget.configuration.appBarBuilder?.call(context) ??
DefaultAppbar(
configuration: configuration,
configuration: widget.configuration,
),
bottomNavigationBar: configuration.bottomNavigationBar,
bottomNavigationBar: widget.configuration.bottomNavigationBar,
body: SafeArea(
child: Padding(
padding: configuration.pagePadding,
padding: widget.configuration.pagePadding,
child: FutureBuilder(
// ignore: discarded_futures
future: configuration.shops(),
builder: (BuildContext context, AsyncSnapshot data) {
if (data.connectionState == ConnectionState.waiting) {
future: widget.configuration.shops(),
builder: (context, snapshot) {
List<Shop>? shops;
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
@ -49,106 +53,47 @@ class ProductPageScreen extends StatelessWidget {
);
}
if (data.hasError) {
return configuration.errorBuilder?.call(
if (snapshot.hasError) {
return widget.configuration.errorBuilder?.call(
context,
data.error,
snapshot.error,
) ??
DefaultError(
error: data.error,
error: snapshot.error,
);
}
List<Shop>? shops = data.data;
shops = snapshot.data;
if (shops == null || shops.isEmpty) {
return configuration.errorBuilder?.call(
return widget.configuration.errorBuilder?.call(
context,
data.error,
snapshot.error,
) ??
DefaultError(error: data.error);
DefaultError(error: snapshot.error);
}
if (initialBuildShopId != null) {
Shop? initialShop;
for (var shop in shops) {
if (shop.id == initialBuildShopId) {
initialShop = shop;
break;
}
if (widget.configuration.initialShopId != null) {
var initialShop = shops.firstWhereOrNull(
(shop) => shop.id == widget.configuration.initialShopId,
);
if (initialShop != null) {
widget.configuration.shoppingService.shopService.selectShop(
initialShop,
);
} else {
widget.configuration.shoppingService.shopService.selectShop(
shops.first,
);
}
configuration.shoppingService.shopService
.selectShop(initialShop ?? shops.first);
} else if (configuration.initialShopId != null) {
Shop? initialShop;
for (var shop in shops) {
if (shop.id == configuration.initialShopId) {
initialShop = shop;
break;
}
}
configuration.shoppingService.shopService
.selectShop(initialShop ?? shops.first);
} else {
configuration.shoppingService.shopService
.selectShop(shops.first);
widget.configuration.shoppingService.shopService.selectShop(
shops.first,
);
}
return ListenableBuilder(
listenable: configuration.shoppingService.shopService,
builder: (BuildContext context, Widget? _) {
configuration.onShopSelectionChange?.call(
configuration.shoppingService.shopService.selectedShop!,
);
return Stack(
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
configuration.shopselectorBuilder?.call(
context,
configuration,
shops,
configuration
.shoppingService.shopService.selectShop,
) ??
ShopSelector(
configuration: configuration,
shops: shops,
onTap: configuration
.shoppingService.shopService.selectShop,
),
configuration.selectedCategoryBuilder?.call(
configuration,
) ??
SelectedCategories(
configuration: configuration,
),
_ShopContents(
configuration: configuration,
),
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: configuration.shoppingCartButtonBuilder != null
? configuration.shoppingCartButtonBuilder!(
context,
configuration,
)
: DefaultShoppingCartButton(
configuration: configuration,
),
),
],
);
},
return _ProductPageContent(
configuration: widget.configuration,
shops: shops,
);
},
),
@ -157,33 +102,130 @@ class ProductPageScreen extends StatelessWidget {
);
}
class _ShopContents extends StatelessWidget {
class _ProductPageContent extends StatefulWidget {
const _ProductPageContent({
required this.configuration,
required this.shops,
});
final ProductPageConfiguration configuration;
final List<Shop> shops;
@override
State<_ProductPageContent> createState() => _ProductPageContentState();
}
class _ProductPageContentState extends State<_ProductPageContent> {
@override
Widget build(BuildContext context) => Stack(
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// shop selector
widget.configuration.shopselectorBuilder?.call(
context,
widget.configuration,
widget.shops,
widget
.configuration.shoppingService.shopService.selectShop,
) ??
ShopSelector(
configuration: widget.configuration,
shops: widget.shops,
onTap: (shop) {
widget.configuration.shoppingService.shopService
.selectShop(shop);
},
),
// selected categories
widget.configuration.selectedCategoryBuilder?.call(
widget.configuration,
) ??
SelectedCategories(
configuration: widget.configuration,
),
// products
_ShopContents(
configuration: widget.configuration,
),
],
),
),
// button
Align(
alignment: Alignment.bottomCenter,
child: widget.configuration.shoppingCartButtonBuilder != null
? widget.configuration.shoppingCartButtonBuilder!(
context,
widget.configuration,
)
: DefaultShoppingCartButton(
configuration: widget.configuration,
),
),
],
);
}
class _ShopContents extends StatefulWidget {
const _ShopContents({
required this.configuration,
});
final ProductPageConfiguration configuration;
@override
State<_ShopContents> createState() => _ShopContentsState();
}
class _ShopContentsState extends State<_ShopContents> {
@override
void initState() {
widget.configuration.shoppingService.shopService.addListener(_listen);
super.initState();
}
@override
void dispose() {
widget.configuration.shoppingService.shopService.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: configuration.pagePadding.horizontal,
horizontal: widget.configuration.pagePadding.horizontal,
),
child: FutureBuilder(
// ignore: discarded_futures
future: configuration.getProducts(
configuration.shoppingService.shopService.selectedShop!,
future: widget.configuration.getProducts(
widget.configuration.shoppingService.shopService.selectedShop!,
),
builder: (context, snapshot) {
List<Product> productPageContent;
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator.adaptive());
return SizedBox(
height: MediaQuery.of(context).size.height * 0.7,
child: const Center(
child: CircularProgressIndicator.adaptive(),
),
);
}
if (snapshot.hasError) {
if (configuration.errorBuilder != null) {
return configuration.errorBuilder!(
if (widget.configuration.errorBuilder != null) {
return widget.configuration.errorBuilder!(
context,
snapshot.error,
);
@ -192,13 +234,11 @@ class _ShopContents extends StatelessWidget {
}
}
List<Product> productPageContent;
productPageContent =
configuration.shoppingService.productService.products;
widget.configuration.shoppingService.productService.products;
if (productPageContent.isEmpty) {
return configuration.noContentBuilder?.call(context) ??
return widget.configuration.noContentBuilder?.call(context) ??
const DefaultNoContent();
}
@ -210,15 +250,15 @@ class _ShopContents extends StatelessWidget {
children: [
// Discounted product
if (discountedproducts.isNotEmpty) ...[
configuration.discountBuilder?.call(
widget.configuration.discountBuilder?.call(
context,
configuration,
widget.configuration,
discountedproducts,
) ??
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: WeeklyDiscount(
configuration: configuration,
configuration: widget.configuration,
product: discountedproducts.first,
),
),
@ -227,15 +267,15 @@ class _ShopContents extends StatelessWidget {
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Text(
configuration.translations.categoryItemListTitle,
widget.configuration.translations.categoryItemListTitle,
style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
),
configuration.categoryListBuilder?.call(
widget.configuration.categoryListBuilder?.call(
context,
configuration,
widget.configuration,
productPageContent,
) ??
Padding(
@ -244,15 +284,11 @@ class _ShopContents extends StatelessWidget {
children: [
// Products
ListenableBuilder(
listenable:
configuration.shoppingService.productService,
builder: (context, _) => getCategoryList(
context,
configuration,
configuration
.shoppingService.productService.products,
),
getCategoryList(
context,
widget.configuration,
widget.configuration.shoppingService.productService
.products,
),
// Bottom padding so the last product is not cut off
@ -268,55 +304,3 @@ class _ShopContents extends StatelessWidget {
);
}
}
/// Selected categories.
class SelectedCategories extends StatelessWidget {
/// Constructor for the selected categories.
const SelectedCategories({
required this.configuration,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return ListenableBuilder(
listenable: configuration.shoppingService.productService,
builder: (context, _) => Padding(
padding: const EdgeInsets.only(left: 4),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var category in configuration
.shoppingService.productService.selectedCategories) ...[
Padding(
padding: const EdgeInsets.only(right: 8),
child: Chip(
backgroundColor: theme.colorScheme.primary,
deleteIcon: const Icon(
Icons.close,
color: Colors.white,
),
onDeleted: () {
configuration.shoppingService.productService
.selectCategory(category);
},
label: Text(
category,
style: theme.textTheme.bodyMedium
?.copyWith(color: Colors.white),
),
),
),
],
],
),
),
),
);
}
}

View file

@ -2,7 +2,7 @@ import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
/// Default shopping cart button for the product page.
class DefaultShoppingCartButton extends StatelessWidget {
class DefaultShoppingCartButton extends StatefulWidget {
/// Constructor for the default shopping cart button for the product page.
const DefaultShoppingCartButton({
required this.configuration,
@ -12,35 +12,56 @@ class DefaultShoppingCartButton extends StatelessWidget {
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
State<DefaultShoppingCartButton> createState() =>
_DefaultShoppingCartButtonState();
}
class _DefaultShoppingCartButtonState extends State<DefaultShoppingCartButton> {
@override
void initState() {
super.initState();
widget.configuration.shoppingService.shoppingCartService
.addListener(_listen);
}
@override
void dispose() {
widget.configuration.shoppingService.shoppingCartService
.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return ListenableBuilder(
listenable: configuration.shoppingService.shoppingCartService,
builder: (context, widget) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: configuration
.shoppingService.shoppingCartService.products.isNotEmpty
? configuration.onNavigateToShoppingCart
: null,
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: widget.configuration.shoppingService.shoppingCartService
.products.isNotEmpty
? widget.configuration.onNavigateToShoppingCart
: null,
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.navigateToShoppingCart,
style: theme.textTheme.displayLarge,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
widget.configuration.translations.navigateToShoppingCart,
style: theme.textTheme.displayLarge,
),
),
),

View file

@ -0,0 +1,72 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
/// Selected categories.
class SelectedCategories extends StatefulWidget {
/// Constructor for the selected categories.
const SelectedCategories({
required this.configuration,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
State<SelectedCategories> createState() => _SelectedCategoriesState();
}
class _SelectedCategoriesState extends State<SelectedCategories> {
@override
void initState() {
widget.configuration.shoppingService.productService.addListener(_listen);
super.initState();
}
@override
void dispose() {
widget.configuration.shoppingService.productService.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(left: 4),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var category in widget.configuration.shoppingService
.productService.selectedCategories) ...[
Padding(
padding: const EdgeInsets.only(right: 8),
child: Chip(
backgroundColor: theme.colorScheme.primary,
deleteIcon: const Icon(
Icons.close,
color: Colors.white,
),
onDeleted: () {
widget.configuration.shoppingService.productService
.selectCategory(category);
},
label: Text(
category,
style: theme.textTheme.bodyMedium
?.copyWith(color: Colors.white),
),
),
),
],
],
),
),
);
}
}

View file

@ -5,7 +5,7 @@ import "package:flutter_product_page/src/widgets/spaced_wrap.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Shop selector widget that displays a list to navigate between shops.
class ShopSelector extends StatelessWidget {
class ShopSelector extends StatefulWidget {
/// Constructor for the shop selector.
const ShopSelector({
required this.configuration,
@ -33,30 +33,53 @@ class ShopSelector extends StatelessWidget {
/// Padding on the buttons.
final double paddingOnButtons;
@override
State<ShopSelector> createState() => _ShopSelectorState();
}
class _ShopSelectorState extends State<ShopSelector> {
@override
void initState() {
widget.configuration.shoppingService.shopService.addListener(_listen);
super.initState();
}
@override
void dispose() {
widget.configuration.shoppingService.shopService.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
if (shops.length == 1) {
if (widget.shops.length == 1) {
return const SizedBox.shrink();
}
if (configuration.shopSelectorStyle == ShopSelectorStyle.spacedWrap) {
if (widget.configuration.shopSelectorStyle ==
ShopSelectorStyle.spacedWrap) {
return SpacedWrap(
shops: shops,
shops: widget.shops,
selectedItem:
configuration.shoppingService.shopService.selectedShop!.id,
onTap: onTap,
widget.configuration.shoppingService.shopService.selectedShop!.id,
onTap: widget.onTap,
width: MediaQuery.of(context).size.width - (16 * 2),
paddingBetweenButtons: paddingBetweenButtons,
paddingOnButtons: paddingOnButtons,
paddingBetweenButtons: widget.paddingBetweenButtons,
paddingOnButtons: widget.paddingOnButtons,
);
}
return HorizontalListItems(
shops: shops,
selectedItem: configuration.shoppingService.shopService.selectedShop!.id,
onTap: onTap,
paddingBetweenButtons: paddingBetweenButtons,
paddingOnButtons: paddingOnButtons,
shops: widget.shops,
selectedItem:
widget.configuration.shoppingService.shopService.selectedShop!.id,
onTap: widget.onTap,
paddingBetweenButtons: widget.paddingBetweenButtons,
paddingOnButtons: widget.paddingOnButtons,
);
}
}

View file

@ -14,7 +14,6 @@ class ShoppingConfiguration {
this.onNavigateToShoppingCart,
this.getProductsInShoppingCart,
this.shoppingCartButtonBuilder,
this.initialShopid,
this.productBuilder,
this.onShopSelectionChange,
this.productPageTranslations,
@ -82,9 +81,6 @@ class ShoppingConfiguration {
final Widget Function(BuildContext, ProductPageConfiguration)?
shoppingCartButtonBuilder;
/// Initial shop that will be selected
final String? initialShopid;
/// ProductPage item builder
final Widget Function(
BuildContext,

View file

@ -37,10 +37,8 @@ class ShoppingProductPage extends StatelessWidget {
Widget build(BuildContext context) {
var service = shoppingConfiguration.shoppingService;
return ProductPageScreen(
initialBuildShopId: shoppingConfiguration.initialShopid,
configuration: ProductPageConfiguration(
shoppingService: service,
initialShopId: shoppingConfiguration.initialShopid,
shoppingCartButtonBuilder:
shoppingConfiguration.shoppingCartButtonBuilder,
productBuilder: shoppingConfiguration.productBuilder,

View file

@ -62,7 +62,17 @@ class LocalProductService with ChangeNotifier implements ProductService {
description: "This is a delicious Brown fish",
),
];
// only return items that match the selectedcategories
_allProducts = List.from(_products);
_products = _products.where((element) {
if (_selectedCategories.isEmpty) {
return true;
}
return _selectedCategories.contains(element.category);
}).toList();
return Future.value(_products);
}