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/material.dart";
import "package:flutter_order_details/flutter_order_details.dart"; import "package:flutter_order_details/flutter_order_details.dart";
/// Default next button for the order details page. /// Default next button for the order details page.
class DefaultNextButton extends StatelessWidget { class DefaultNextButton extends StatelessWidget {
/// Constructor for the default next button for the order details page. /// 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. /// Configuration for the order details page.
final OrderDetailConfiguration configuration; final OrderDetailConfiguration configuration;
/// Controller for the form. /// Controller for the form.
final FlutterFormController controller; final FlutterFormController controller;
/// Current step in the form. /// Current step in the form.
final int currentStep; final int currentStep;
/// Whether the form is checking pages. /// Whether the form is checking pages.
final bool checkingPages; final bool checkingPages;
@override @override

View file

@ -1,3 +1,4 @@
import "package:collection/collection.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/services/category_service.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_error.dart";
import "package:flutter_product_page/src/widgets/defaults/default_no_content.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/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/shop_selector.dart";
import "package:flutter_product_page/src/widgets/weekly_discount.dart"; import "package:flutter_product_page/src/widgets/weekly_discount.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A page that displays products. /// A page that displays products.
class ProductPageScreen extends StatelessWidget { class ProductPageScreen extends StatefulWidget {
/// Constructor for the product page. /// Constructor for the product page.
const ProductPageScreen({ const ProductPageScreen({
required this.configuration, required this.configuration,
this.initialBuildShopId,
super.key, super.key,
}); });
/// Configuration for the product page. /// Configuration for the product page.
final ProductPageConfiguration configuration; final ProductPageConfiguration configuration;
/// An optional initial shop ID to select. This overrides the initialShopId @override
/// from the configuration. State<ProductPageScreen> createState() => _ProductPageScreenState();
final String? initialBuildShopId; }
class _ProductPageScreenState extends State<ProductPageScreen> {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: configuration.appBarBuilder?.call(context) ?? appBar: widget.configuration.appBarBuilder?.call(context) ??
DefaultAppbar( DefaultAppbar(
configuration: configuration, configuration: widget.configuration,
), ),
bottomNavigationBar: configuration.bottomNavigationBar, bottomNavigationBar: widget.configuration.bottomNavigationBar,
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: configuration.pagePadding, padding: widget.configuration.pagePadding,
child: FutureBuilder( child: FutureBuilder(
// ignore: discarded_futures // ignore: discarded_futures
future: configuration.shops(), future: widget.configuration.shops(),
builder: (BuildContext context, AsyncSnapshot data) { builder: (context, snapshot) {
if (data.connectionState == ConnectionState.waiting) { List<Shop>? shops;
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column( return const Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -49,106 +53,47 @@ class ProductPageScreen extends StatelessWidget {
); );
} }
if (data.hasError) { if (snapshot.hasError) {
return configuration.errorBuilder?.call( return widget.configuration.errorBuilder?.call(
context, context,
data.error, snapshot.error,
) ?? ) ??
DefaultError( DefaultError(
error: data.error, error: snapshot.error,
); );
} }
List<Shop>? shops = data.data; shops = snapshot.data;
if (shops == null || shops.isEmpty) { if (shops == null || shops.isEmpty) {
return configuration.errorBuilder?.call( return widget.configuration.errorBuilder?.call(
context, context,
data.error, snapshot.error,
) ?? ) ??
DefaultError(error: data.error); DefaultError(error: snapshot.error);
} }
if (initialBuildShopId != null) { if (widget.configuration.initialShopId != null) {
Shop? initialShop; var initialShop = shops.firstWhereOrNull(
(shop) => shop.id == widget.configuration.initialShopId,
for (var shop in shops) { );
if (shop.id == initialBuildShopId) { if (initialShop != null) {
initialShop = shop; widget.configuration.shoppingService.shopService.selectShop(
break; 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 { } else {
configuration.shoppingService.shopService widget.configuration.shoppingService.shopService.selectShop(
.selectShop(shops.first); shops.first,
);
} }
return _ProductPageContent(
return ListenableBuilder( configuration: widget.configuration,
listenable: configuration.shoppingService.shopService, shops: shops,
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,
),
),
],
);
},
); );
}, },
), ),
@ -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({ const _ShopContents({
required this.configuration, required this.configuration,
}); });
final ProductPageConfiguration 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return Padding( return Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: configuration.pagePadding.horizontal, horizontal: widget.configuration.pagePadding.horizontal,
), ),
child: FutureBuilder( child: FutureBuilder(
// ignore: discarded_futures // ignore: discarded_futures
future: configuration.getProducts( future: widget.configuration.getProducts(
configuration.shoppingService.shopService.selectedShop!, widget.configuration.shoppingService.shopService.selectedShop!,
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
List<Product> productPageContent;
if (snapshot.connectionState == ConnectionState.waiting) { 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 (snapshot.hasError) {
if (configuration.errorBuilder != null) { if (widget.configuration.errorBuilder != null) {
return configuration.errorBuilder!( return widget.configuration.errorBuilder!(
context, context,
snapshot.error, snapshot.error,
); );
@ -192,13 +234,11 @@ class _ShopContents extends StatelessWidget {
} }
} }
List<Product> productPageContent;
productPageContent = productPageContent =
configuration.shoppingService.productService.products; widget.configuration.shoppingService.productService.products;
if (productPageContent.isEmpty) { if (productPageContent.isEmpty) {
return configuration.noContentBuilder?.call(context) ?? return widget.configuration.noContentBuilder?.call(context) ??
const DefaultNoContent(); const DefaultNoContent();
} }
@ -210,15 +250,15 @@ class _ShopContents extends StatelessWidget {
children: [ children: [
// Discounted product // Discounted product
if (discountedproducts.isNotEmpty) ...[ if (discountedproducts.isNotEmpty) ...[
configuration.discountBuilder?.call( widget.configuration.discountBuilder?.call(
context, context,
configuration, widget.configuration,
discountedproducts, discountedproducts,
) ?? ) ??
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: WeeklyDiscount( child: WeeklyDiscount(
configuration: configuration, configuration: widget.configuration,
product: discountedproducts.first, product: discountedproducts.first,
), ),
), ),
@ -227,15 +267,15 @@ class _ShopContents extends StatelessWidget {
padding: padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 24), const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Text( child: Text(
configuration.translations.categoryItemListTitle, widget.configuration.translations.categoryItemListTitle,
style: theme.textTheme.titleLarge, style: theme.textTheme.titleLarge,
textAlign: TextAlign.start, textAlign: TextAlign.start,
), ),
), ),
configuration.categoryListBuilder?.call( widget.configuration.categoryListBuilder?.call(
context, context,
configuration, widget.configuration,
productPageContent, productPageContent,
) ?? ) ??
Padding( Padding(
@ -244,15 +284,11 @@ class _ShopContents extends StatelessWidget {
children: [ children: [
// Products // Products
ListenableBuilder( getCategoryList(
listenable: context,
configuration.shoppingService.productService, widget.configuration,
builder: (context, _) => getCategoryList( widget.configuration.shoppingService.productService
context, .products,
configuration,
configuration
.shoppingService.productService.products,
),
), ),
// Bottom padding so the last product is not cut off // 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"; import "package:flutter_product_page/flutter_product_page.dart";
/// Default shopping cart button for the product page. /// 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. /// Constructor for the default shopping cart button for the product page.
const DefaultShoppingCartButton({ const DefaultShoppingCartButton({
required this.configuration, required this.configuration,
@ -12,35 +12,56 @@ class DefaultShoppingCartButton extends StatelessWidget {
/// Configuration for the product page. /// Configuration for the product page.
final ProductPageConfiguration configuration; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return ListenableBuilder( return Padding(
listenable: configuration.shoppingService.shoppingCartService, padding: const EdgeInsets.symmetric(horizontal: 60),
builder: (context, widget) => Padding( child: SizedBox(
padding: const EdgeInsets.symmetric(horizontal: 60), width: double.infinity,
child: SizedBox( child: FilledButton(
width: double.infinity, onPressed: widget.configuration.shoppingService.shoppingCartService
child: FilledButton( .products.isNotEmpty
onPressed: configuration ? widget.configuration.onNavigateToShoppingCart
.shoppingService.shoppingCartService.products.isNotEmpty : null,
? configuration.onNavigateToShoppingCart style: theme.filledButtonTheme.style?.copyWith(
: null, backgroundColor: WidgetStateProperty.all(
style: theme.filledButtonTheme.style?.copyWith( theme.colorScheme.primary,
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
), ),
child: Padding( ),
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: 16.0, padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 16.0,
), vertical: 12,
child: Text( ),
configuration.translations.navigateToShoppingCart, child: Text(
style: theme.textTheme.displayLarge, 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"; import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Shop selector widget that displays a list to navigate between shops. /// Shop selector widget that displays a list to navigate between shops.
class ShopSelector extends StatelessWidget { class ShopSelector extends StatefulWidget {
/// Constructor for the shop selector. /// Constructor for the shop selector.
const ShopSelector({ const ShopSelector({
required this.configuration, required this.configuration,
@ -33,30 +33,53 @@ class ShopSelector extends StatelessWidget {
/// Padding on the buttons. /// Padding on the buttons.
final double paddingOnButtons; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (shops.length == 1) { if (widget.shops.length == 1) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
if (configuration.shopSelectorStyle == ShopSelectorStyle.spacedWrap) { if (widget.configuration.shopSelectorStyle ==
ShopSelectorStyle.spacedWrap) {
return SpacedWrap( return SpacedWrap(
shops: shops, shops: widget.shops,
selectedItem: selectedItem:
configuration.shoppingService.shopService.selectedShop!.id, widget.configuration.shoppingService.shopService.selectedShop!.id,
onTap: onTap, onTap: widget.onTap,
width: MediaQuery.of(context).size.width - (16 * 2), width: MediaQuery.of(context).size.width - (16 * 2),
paddingBetweenButtons: paddingBetweenButtons, paddingBetweenButtons: widget.paddingBetweenButtons,
paddingOnButtons: paddingOnButtons, paddingOnButtons: widget.paddingOnButtons,
); );
} }
return HorizontalListItems( return HorizontalListItems(
shops: shops, shops: widget.shops,
selectedItem: configuration.shoppingService.shopService.selectedShop!.id, selectedItem:
onTap: onTap, widget.configuration.shoppingService.shopService.selectedShop!.id,
paddingBetweenButtons: paddingBetweenButtons, onTap: widget.onTap,
paddingOnButtons: paddingOnButtons, paddingBetweenButtons: widget.paddingBetweenButtons,
paddingOnButtons: widget.paddingOnButtons,
); );
} }
} }

View file

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

View file

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

View file

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