sindev
sindev

Reputation: 1

Flutter BLoC - state doesn't triggers widget rebuild

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

Answers (1)

Eugenio Amato
Eugenio Amato

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

Related Questions