From 0e7e2ff0e573a926d6f2295e2d4614377ee7e5f8 Mon Sep 17 00:00:00 2001 From: mike doornenbal Date: Tue, 9 Jul 2024 15:50:25 +0200 Subject: [PATCH] feat: remove listenablebuilder from product page --- .../lib/src/widgets/default_next_button.dart | 4 +- .../lib/src/product_page_screen.dart | 334 +++++++++--------- .../default_shopping_cart_button.dart | 71 ++-- .../widgets/defaults/selected_categories.dart | 72 ++++ .../lib/src/widgets/shop_selector.dart | 49 ++- .../configuration/shopping_configuration.dart | 4 - .../flutter_shopping_navigator_userstory.dart | 2 - .../lib/service/local_product_service.dart | 10 + 8 files changed, 326 insertions(+), 220 deletions(-) create mode 100644 packages/flutter_product_page/lib/src/widgets/defaults/selected_categories.dart diff --git a/packages/flutter_order_details/lib/src/widgets/default_next_button.dart b/packages/flutter_order_details/lib/src/widgets/default_next_button.dart index 7a1db09..290fb9d 100644 --- a/packages/flutter_order_details/lib/src/widgets/default_next_button.dart +++ b/packages/flutter_order_details/lib/src/widgets/default_next_button.dart @@ -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 diff --git a/packages/flutter_product_page/lib/src/product_page_screen.dart b/packages/flutter_product_page/lib/src/product_page_screen.dart index 70d174c..6cc5303 100644 --- a/packages/flutter_product_page/lib/src/product_page_screen.dart +++ b/packages/flutter_product_page/lib/src/product_page_screen.dart @@ -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 createState() => _ProductPageScreenState(); +} +class _ProductPageScreenState extends State { @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? 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? 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 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 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 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), - ), - ), - ), - ], - ], - ), - ), - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/defaults/default_shopping_cart_button.dart b/packages/flutter_product_page/lib/src/widgets/defaults/default_shopping_cart_button.dart index e44a282..1645ca9 100644 --- a/packages/flutter_product_page/lib/src/widgets/defaults/default_shopping_cart_button.dart +++ b/packages/flutter_product_page/lib/src/widgets/defaults/default_shopping_cart_button.dart @@ -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 createState() => + _DefaultShoppingCartButtonState(); +} + +class _DefaultShoppingCartButtonState extends State { + @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, ), ), ), diff --git a/packages/flutter_product_page/lib/src/widgets/defaults/selected_categories.dart b/packages/flutter_product_page/lib/src/widgets/defaults/selected_categories.dart new file mode 100644 index 0000000..2e5ee9a --- /dev/null +++ b/packages/flutter_product_page/lib/src/widgets/defaults/selected_categories.dart @@ -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 createState() => _SelectedCategoriesState(); +} + +class _SelectedCategoriesState extends State { + @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), + ), + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/packages/flutter_product_page/lib/src/widgets/shop_selector.dart b/packages/flutter_product_page/lib/src/widgets/shop_selector.dart index 1894aca..4ed4090 100644 --- a/packages/flutter_product_page/lib/src/widgets/shop_selector.dart +++ b/packages/flutter_product_page/lib/src/widgets/shop_selector.dart @@ -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 createState() => _ShopSelectorState(); +} + +class _ShopSelectorState extends State { + @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, ); } } diff --git a/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart b/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart index 0b2f55a..2cb20bb 100644 --- a/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart +++ b/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart @@ -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, diff --git a/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart b/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart index 9b7fbb6..b19bbb7 100644 --- a/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart +++ b/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart @@ -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, diff --git a/packages/flutter_shopping_local/lib/service/local_product_service.dart b/packages/flutter_shopping_local/lib/service/local_product_service.dart index 69c3f4f..bf67bef 100644 --- a/packages/flutter_shopping_local/lib/service/local_product_service.dart +++ b/packages/flutter_shopping_local/lib/service/local_product_service.dart @@ -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); }