Reputation: 1
the app is simple categories/products display , everything works fine except select a product from a category products and swip back to the products widget , the state changes and it's neither one of the states i created and just shows a loading indicator ( ProductsWrapper default return from state).
so here is the code :
ProductBloc :
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final ProductRepository productRepository;
ProductBloc({required this.productRepository}) : super(ProductsEmpty());
@override
Stream<Transition<ProductEvent, ProductState>> transformEvents(
Stream<ProductEvent> events,
TransitionFunction<ProductEvent, ProductState> transitionFn) {
return super.transformEvents(
events.debounceTime(const Duration(microseconds: 500)), transitionFn);
}
@override
Stream<ProductState> mapEventToState(ProductEvent event) async* {
if (event is FetchProducts) {
yield* _mapFetchProductsToState(event);
} else if (event is RefreshProducts) {
yield* _mapRefreshProductsToState(event);
} else if (event is FetchProduct) {
yield* _mapFetchProductToState(event);
} else if (event is RefreshProduct) {
yield* _mapRefreshProductToState(event);
}
}
Stream<ProductState> _mapFetchProductsToState(FetchProducts event) async* {
try {
final products =
(await productRepository.getCategoryProducts(event.categoryId));
yield ProductsLoaded(products: products.products!);
} catch (_) {
yield state;
}
}
Stream<ProductState> _mapRefreshProductsToState(
RefreshProducts event) async* {
try {
final products =
await productRepository.getCategoryProducts(event.categoryId);
yield ProductsLoaded(products: products.products!);
return;
} catch (_) {
yield state;
}
}
Stream<ProductState> _mapFetchProductToState(FetchProduct event) async* {
try {
final product =
(await productRepository.getProductDetails(event.productId));
yield ProductLoaded(product: product);
} catch (e) {
yield state;
}
}
Stream<ProductState> _mapRefreshProductToState(RefreshProduct event) async* {
try {
final product =
await productRepository.getProductDetails(event.productId);
yield ProductLoaded(product: product);
return;
} catch (_) {
yield state;
}
}
}
states :
abstract class ProductState extends Equatable {
const ProductState();
@override
List<Object?> get props => [];
}
class ProductsEmpty extends ProductState {}
class ProductEmpty extends ProductState {}
class ProductLoading extends ProductState {}
class ProductsLoading extends ProductState {}
class ProductLoaded extends ProductState {
final Product product;
const ProductLoaded({required this.product});
ProductLoaded copyWith({required Product product}) {
return ProductLoaded(product: product);
}
@override
List<Object?> get props => [product];
@override
String toString() => 'ProductLoaded { product: ${product.name}}';
}
class ProductsLoaded extends ProductState {
final List<Product> products;
const ProductsLoaded({required this.products});
ProductsLoaded copyWith({required List<Product> products}) {
return ProductsLoaded(products: products);
}
@override
List<Object?> get props => [products];
@override
String toString() => 'ProductLoaded { products: ${products.length}}';
}
class ProductError extends ProductState {}
ProductRepository ( ProductApiService is just the api and it's working fine ) :
class ProductRepository {
final ProductApiService productApiService;
ProductRepository({ProductApiService? productApiService})
: productApiService = productApiService ?? ProductApiService();
Future<Products> getCategoryProducts(int? categoryId) async {
return productApiService.fetchCategoryProducts(categoryId);
}
Future<Product> getProductDetails(int? productId) async {
return productApiService.fetchProductDetails(productId);
}
}
ProductsWrapper :
final int? categoryId;
const ProductsWrapper({Key? key, required this.categoryId}) : super(key: key);
@override
_ProductsWrapperState createState() => _ProductsWrapperState();
}
class _ProductsWrapperState extends State<ProductsWrapper> {
final _scrollController = ScrollController();
final _scrollThreshold = 200;
Completer _productsRefreshCompleter = new Completer();
List<Product> products = [];
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _onScroll() {
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
if (maxScroll - currentScroll <= _scrollThreshold) {
context
.read<ProductBloc>()
.add(FetchProducts(categoryId: widget.categoryId!));
}
}
@override
void initState() {
super.initState();
context
.read<ProductBloc>()
.add(FetchProducts(categoryId: widget.categoryId!));
_scrollController.addListener(_onScroll);
_productsRefreshCompleter = Completer();
}
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
final double itemHeight = 260;
final double itemWidth = size.width / 2;
return Scaffold(
key: _scaffoldKey,
body: BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductsLoaded) {
products = state.products;
_productsRefreshCompleter.complete();
}
},
child: Container(
margin: EdgeInsets.all(8.0),
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductsLoading) {
print('a7a');
return Center(
child: LoadingIndicator(),
);
}
if (state is ProductsLoaded) {
products = state.products;
if (state.products.isEmpty) {
return Center(
child: Text("No Products Found in this category"),
);
}
return Scaffold(
body: SafeArea(
child: Container(
child: GridView.builder(
itemCount: products.length,
scrollDirection: Axis.vertical,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio:
(itemWidth / itemHeight)),
itemBuilder: (context, index) => Card(
elevation: 0,
child: InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ProductDetailScreen(
productId:
products[index]
.id)));
},
child: Container(
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
ClipRRect(
child: Image.network(
products[index]
.image!
.image
.toString(),
height: 150,
fit: BoxFit.fitWidth,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
products[index].name.toString(),
style: TextStyle(
color: Colors.black,
fontWeight:
FontWeight.bold),
),
),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
'\$${products[index].price.toString()}'),
),
Padding(
padding: EdgeInsets.only(
right: 8.0),
child: CircleAvatar(
backgroundColor:
Theme.of(context)
.primaryColor,
radius: 10,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.add,
size: 20,
),
color: Colors.white,
onPressed: () {},
),
),
)
],
)
],
),
),
),
)),
),
),
);
}
return Center(
child: LoadingIndicator(strokeWidth: 5.0,),
);
}))));
}
}
ProductDetailScreen :
class ProductDetailScreen extends StatefulWidget {
final int? productId;
const ProductDetailScreen({Key? key, required this.productId})
: super(key: key);
@override
_ProductDetailScreenState createState() => _ProductDetailScreenState();
}
class _ProductDetailScreenState extends State<ProductDetailScreen> {
Completer _productRefreshCompleter = new Completer();
Product product = new Product();
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
context.read<ProductBloc>().add(FetchProduct(productId: widget.productId));
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductLoaded) {
product = state.product;
_productRefreshCompleter.complete();
_productRefreshCompleter = Completer();
}
},
child: Container(
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductLoading) {
return Center(
child: LoadingIndicator(),
);
}
if (state is ProductLoaded) {
return Scaffold(
body: SafeArea(
child: Container(
child: Text(product.name.toString()),
),
),
);
}
return Center(
child: LoadingIndicator(
strokeWidth: 5.0,
),
);
},
),
),
),
);
}
}
any help is appreciated .
thanks for taking time reading this , have a nice day and stay safe.
Upvotes: 0
Views: 881
Reputation: 11
The problem is that you are using one bloc to do 2 things. The products list is an entity, the single detail is another entity. And you need to use the properties of the states as a result inside blocBuilders. Plus, you don't need any listener and completer. The bloc pattern refreshes all when state changes. I have created a repo with a working solution. https://github.com/eugenioamato/categoryproducts
Upvotes: 1