findById returns null on hot reload

Viewed 272

I have the following dart class which loads an object and an image. When I perform hot reload, the function findById returns an error message Bad State: No element. Upon debugging, I see that the list that I am querying on is null once I do a hot realod. I want to know why that happens and how do I fix this?

class ProductDetailScreen extends StatefulWidget {
   static const routeName = '/product-detail';

  @override
  _ProductDetailScreenState createState() => _ProductDetailScreenState();
}

class _ProductDetailScreenState extends State<ProductDetailScreen> {
 var loadedProduct; 
 var productId;
 var _isLoading; 
  var _imageUrl;
  var cartId; 
  var imagePath = '';
   @override
  void initState() {
    super.initState();
    createCartId();
  }
   
   Future<void> createCartId()async {
        var id = await nanoid(10);
        cartId = id; 
   }

   Future<void> didChangeDependencies() async {
       productId = ModalRoute.of(context).settings.arguments as String; 
       imagePath = productId;
       setState(() {
          _isLoading = true; 
       });
      
       final myCacheManager = MyCacheManager();
    await myCacheManager.cacheImage(imagePath).then((String imageUrl) {
      setState(() {
        _imageUrl = imageUrl;
         _isLoading = false; 
      });    
    });
  
  }
  @override
  Widget build(BuildContext context) {
    loadedProduct = Provider.of<Products>(context,listen:false).findById(productId); 
    Size size = MediaQuery.of(context).size;
    var userId = Provider.of<Auth>(context, listen: false).userId;
     
    return !(_isLoading)?Scaffold(
      appBar: AppBar(
         actions: null,
        title: Text(loadedProduct.title),
      ),
      body:
      CustomScrollView(
        slivers: [
    SliverFillRemaining(
      hasScrollBody: false,
      child:
      Column(
      children: <Widget>[
        Container(
              height: size.height * 0.35,
              width: double.infinity,
           child:_imageUrl != 'No picture loaded'?
           CachedNetworkImage(
                imageUrl: _imageUrl,
                progressIndicatorBuilder: (context, url, downloadProgress) => 
                 Center(child:Loading()),
                errorWidget: (context, url, error) => Image.asset('assets/images/placeholder.png'),
              )
            :Image.asset('assets/images/placeholder.png'),
      ),
        Expanded(
          child: ItemInfo(
          cartId,
          loadedProduct.id,
          loadedProduct.title,
          loadedProduct.price,
          loadedProduct.description,
          loadedProduct.categoryName, 
          loadedProduct.isAvailable),
        ),
      ],
    ))]))
    :Center(child:Loading());
  }
}

class ItemInfo extends StatefulWidget {
  
   ItemInfo(this.cartId,this.id,this.name,this.price,this.description,this.category,this.isAvailable);
  var id;
  var cartId;
  var name;
  var price;
  var description;
  var category;
  var isAvailable;

  @override
  _ItemInfoState createState() => _ItemInfoState();
}

class _ItemInfoState extends State<ItemInfo> {
 bool changePrice = false;
 List<String> selectedItems; 
  @override
  Widget build(BuildContext context) {
    return 
      Container(
      padding: EdgeInsets.all(15),
      width: double.infinity,
      decoration: BoxDecoration(
        boxShadow: [BoxShadow(color: Colors.orange, spreadRadius: 1)],
        color: Colors.white,
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(30),
          topRight: Radius.circular(30),
        ),
      ),
      child:
        Column(
        children: <Widget>[
            TitlePriceRating(
            name: widget.name,
            price: widget.price,
          ),        
        ],
      ),
    );
  }
}

Here's the function function findById

Product findById(String id) {
    
    return _items.firstWhere((prod) => prod.id == id);
  }

Here's the function which sets the _items list:

Future<void> fetchAndSetProducts(String title) async {
    var encodedTitle = Uri.encodeComponent(title);  
    var url = 'https://mm-nn-default-rtdb.firebaseio.com/products.json?auth=$authToken';
    try {
        final response = await http.get(Uri.parse(url));
        final extractedData = json.decode(response.body) as Map<String, dynamic>;
        final List<Product> loadedProducts = [];

        if (extractedData == null || extractedData['error'] != null) {
          _items = loadedProducts;
          return _items;
     }
      extractedData.forEach((prodId, prodData) {
        loadedProducts.add(Product(
          id: prodId,
          title: prodData['title'],
          description: prodData['description'],
          price: prodData['price'],
          categoryName: prodData['categoryName'],
        ));
      });
     notifyListeners();  
      _items = loadedProducts; 
      return _items;
    } catch (error) {
      throw (error);
    }
  }

This function is called in a class called ProductOverviewScreen which then calls a class called ProductGrid. ProductGrid basically contains a listviewbuilder of the products. This class then calls ProductItem which is basically a single product in the product list. ProductItem contains a button which when clicked, calls ProductDetailsScreen which is the class causing the error.

3 Answers

Use setState method.

   Future<void> didChangeDependencies() async {
       setState(() {
          productId = ModalRoute.of(context).settings.arguments as String; 
          imagePath = productId;
          _isLoading = true; 
       });
      
       final myCacheManager = MyCacheManager();
       await myCacheManager.cacheImage(imagePath).then((String imageUrl) {
          setState(() {
             _imageUrl = imageUrl;
             _isLoading = false; 
          });    
       });
    }

replace

return _items.firstWhere((prod) => prod.id == id);

with

return _items.firstWhere((prod) => prod.id == id, orElse: () => null,);

The problem is in this code.

 extractedData.forEach((prodId, prodData) {
    loadedProducts.add(Product(
      id: prodId,
      title: prodData['title'],
      description: prodData['description'],
      price: prodData['price'],
      categoryName: prodData['categoryName'],
    ));
  });

In this code, you are directly adding the product to the state. The state is immutable. So make a temporary list there add all the data into the temporary list and finally use setState() to put the temporary list data into the state. The code should be like this.

List<Product> tempList = [];
extractedData.forEach((prodId, prodData) {
    tempList.add(Product(
      id: prodId,
      title: prodData['title'],
      description: prodData['description'],
      price: prodData['price'],
      categoryName: prodData['categoryName'],
    ));
  });
setState((){
  loadedProducts = tempList;
})
Related