How do I prevent an endless Listview scroll of product data being repeated on my product screens? [Flutter]

Viewed 68

I'm working on a shopping app for a school project, and I'm trying to get my product images and details to show up on the various product detail screens. I used some sample images from flutter's Shrine sample app as I followed their tutorial. The problem I'm facing now is an endless Listview scroll that looks like this, and when you keep scrolling, it repeats the image and details endlessly: enter image description here

What should I do to avoid this? Sorry, I am really new to coding so I am not to sure how to go about fixing this problem...Below I have included a few dart files of my current code which might be helpful. Thank you to anyone who is willing to help, it is very much appreciated!

Product detail screen dart file:

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:MyShoppingApp/provider/CartProvider.dart';
import 'package:MyShoppingApp/db/cart_database.dart';
import 'package:MyShoppingApp/model/cart.dart';
import 'model/products_repository.dart';
import '../model/cart.dart';


class ProductDetailsPage extends StatelessWidget {

  static const routeName = '/user-products';
   ProductDetailsPage({Key? key}) : super(key: key); //const
  DBHelper dbHelper = DBHelper();

  @override
  Widget build(BuildContext context) {
    //get particular productId using the ModalRoute class
    final productId = ModalRoute.of(context)!.settings.arguments as String;
    print(productId);
    //use Provider package to find out ID by accessing method declared in Product()
    final loadedProduct = ProductsRepository().findById(productId);
   
    //List<bool> clicked = List.generate(10, (index) => false, growable: true);
      final cart = Provider.of<CartProvider>(context);
      void saveData(int index) {
        dbHelper
            .insert(
          CartItem(
            id: index,
            title: loadedProduct.name,
            price: loadedProduct.price.toDouble(),
            quantity: ValueNotifier(1),
            image: loadedProduct.image,
          ),
        )
            .then((value) {
          cart.addTotalPrice(loadedProduct.price.toDouble());
          cart.addCounter();
          print('Product Added to cart');
        }).onError((error, stackTrace) {
          print(error.toString());
        });
      }

    return Scaffold(
      backgroundColor: Colors.orange[50],
      appBar: AppBar(
        backgroundColor: Colors.deepOrange[900],
        title: const Text("Product details "),
        leading: IconButton(
          icon: const Icon(
            Icons.arrow_back_ios_outlined,
            color: Colors.black,
            semanticLabel: 'back to home',
          ),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
        //body:
        body: ListView.builder(
            padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0),
            shrinkWrap: true,
            itemCount: loadedProduct.length,
            itemBuilder: (context, index) {
              return Card(
                //SingleChildScrollView(
                child: Column(
                  children: <Widget>[
                    SizedBox(
                      height: 300,
                      width: double.infinity,
                      child: Image.asset(
                        loadedProduct.image,
                        fit: BoxFit.cover,
                      ),
                    ),
                    const SizedBox(height: 10),
                    Text(
                      '\$${loadedProduct.price}',
                      style: const TextStyle(
                        color: Colors.grey,
                        fontSize: 20,
                      ),
                    ),
                    const SizedBox(
                      height: 10,
                    ),
                    ElevatedButton(
                        style: ElevatedButton.styleFrom(
                            primary: Colors.blueGrey.shade900),
                        onPressed: () {
                          saveData(Random().nextInt(1000));
                        },
                        child: const Text('Add to Cart')),
                    Container(
                      padding: const EdgeInsets.symmetric(horizontal: 10),
                      width: double.infinity,
                      child: Text(
                        loadedProduct.description,
                        textAlign: TextAlign.center,
                        softWrap: true,
                      ),
                    ),
                  ],
                ),
              );
            })
    );
  }
  }

Product Repository Dart file:

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:MyShoppingApp/db/cart_database.dart';
//add product data
import 'package:MyShoppingApp/model/product.dart';

//to get all products at once or any particular product by its ID
//product class that uses mixins with ChangeNotifier
class ProductsRepository with ChangeNotifier {
  DBHelper dbHelper = DBHelper();

  static List<Product> loadProducts(Category category) {
    //linked list storing objects of type Product
    var allProducts = <Product>[
      Product(
        category: Category.accessories,
        id: "0",
        isFeatured: true,
        name: 'Vagabond sack',
        price: 120,
        details: "Nice fancy shirt",
        description: "Comfortable and minimalistic",
        image: "packages/shrine_images/0-0.jpg",
      ),
      Product(
        category: Category.accessories,
        id: "1",
        isFeatured: true,
        name: 'Stella sunglasses',
        price: 58,
        details: "",
        description: "",
        image: "packages/shrine_images/1-0.jpg",
      ),
      Product(
        category: Category.accessories,
        id: "2",
        isFeatured: false,
        name: 'Whitney belt',
        price: 35,
        details: "",
        description: "",
        image: "packages/shrine_images/2-0.jpg",
      ),
      Product(
        category: Category.accessories,
        id: "3",
        isFeatured: true,
        name: 'Garden strand',
        price: 98,
        details: "",
        description: "",
        image: "packages/shrine_images/3-0.jpg",
      ),
      Product(
        category: Category.accessories,
        id: "4",
        isFeatured: false,
        name: 'Strut earrings',
        price: 34,
        details: "",
        description: "",
        image: "packages/shrine_images/4-0.jpg",
      ),//removed other products to save space
    ];

    if (category == Category.all) {
      return allProducts;
    } else {
      return allProducts.where((Product p) {
        return p.category == category;
      }).toList();
    }
  }

  //to get particular products by ID
  Product findById(String id) {
    var x = loadProducts(Category.all).firstWhere((prod) => prod.id == id);
    print("findById successful");
    print(x);
    return x;
  }
  
  void addProduct() {
    // _items.add(value);
    notifyListeners();
  }

}

Thank you everyone! Also, this problem stemmed from an earlier error in another post that a user was helping me to work through, but I didn't want to trouble them too much so I am reposting it here. Thank you @eamirho3ein

3 Answers

Try this:

class ProductDetailsPage extends StatefulWidget {

  static const routeName = '/user-products';
   ProductDetailsPage({Key? key}) : super(key: key); 
  @override
  State<ProductDetailsPage> createState() => _ProductDetailsPageState();
}

class _ProductDetailsPageState extends State<ProductDetailsPage> {
 //const
  DBHelper dbHelper = DBHelper();
  List<Product> loadedProduct = []; // <----update this

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      //get particular productId using the ModalRoute class
      final productId = ModalRoute.of(context)!.settings.arguments as String;
      print(productId);
      //use Provider package to find out ID by accessing method declared in Product()
      setState(() {
         loadedProduct = ProductsRepository().findById(productId);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    
    
    //List<bool> clicked = List.generate(10, (index) => false, growable: true);
      final cart = Provider.of<CartProvider>(context);
      void saveData(int index) {
        dbHelper
            .insert(
          CartItem(
            id: index,
            title: loadedProduct.name,
            price: loadedProduct.price.toDouble(),
            quantity: ValueNotifier(1),
            image: loadedProduct.image,
          ),
        )
            .then((value) {
          cart.addTotalPrice(loadedProduct.price.toDouble());
          cart.addCounter();
          print('Product Added to cart');
        }).onError((error, stackTrace) {
          print(error.toString());
        });
      }

    return Scaffold(
      backgroundColor: Colors.orange[50],
      appBar: AppBar(
        backgroundColor: Colors.deepOrange[900],
        title: const Text("Product details "),
        leading: IconButton(
          icon: const Icon(
            Icons.arrow_back_ios_outlined,
            color: Colors.black,
            semanticLabel: 'back to home',
          ),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
        //body:
        body: ListView.builder(
            padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0),
            shrinkWrap: true,
            itemCount: loadedProduct.length,
            itemBuilder: (context, index) {
              return Card(
                //SingleChildScrollView(
                child: Column(
                  children: <Widget>[
                    SizedBox(
                      height: 300,
                      width: double.infinity,
                      child: Image.asset(
                        loadedProduct.image,
                        fit: BoxFit.cover,
                      ),
                    ),
                    const SizedBox(height: 10),
                    Text(
                      '\$${loadedProduct.price}',
                      style: const TextStyle(
                        color: Colors.grey,
                        fontSize: 20,
                      ),
                    ),
                    const SizedBox(
                      height: 10,
                    ),
                    ElevatedButton(
                        style: ElevatedButton.styleFrom(
                            primary: Colors.blueGrey.shade900),
                        onPressed: () {
                          saveData(Random().nextInt(1000));
                        },
                        child: const Text('Add to Cart')),
                    Container(
                      padding: const EdgeInsets.symmetric(horizontal: 10),
                      width: double.infinity,
                      child: Text(
                        loadedProduct.description,
                        textAlign: TextAlign.center,
                        softWrap: true,
                      ),
                    ),
                  ],
                ),
              );
            })
    );
  }
}

Hello I see some weird thing there.

ProductsRepository().findById(productId) return a single Product. Then loadedProduct is a Product. Later on the ListView.builder on the itemCount parameter you used loadedProduct.length (loadedProduct in this point is used as a list of element). I dont know if the Product object have a .length method nor what returns that. But i think there is the problem. When you set itemCount: loadedProduct.length you are saying than the list will have x elements and later on the itemBuilder you are only using the data from loadedProduct to create the same widget in the list, x times.

Assuming that loadedProduct.length = 10, you will have a list view with the same Element 10 times

The next example use the loadedProduct to fill the products list. The product list will be used as the data on the ListView.builder. Then the list will show 3 time the same widget.

final products = [
  loadedProduct,
  loadedProduct,
  loadedProduct
];
return Scaffold(
    backgroundColor: Colors.orange[50],
    appBar: AppBar(
      backgroundColor: Colors.deepOrange[900],
      title: const Text("Product details "),
      leading: IconButton(
        icon: const Icon(
          Icons.arrow_back_ios_outlined,
          color: Colors.black,
          semanticLabel: 'back to home',
        ),
        onPressed: () {
          Navigator.pop(context);
        },
      ),
    ),
    //body:
    body: ListView.builder(
        padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0),
        shrinkWrap: true,
        itemCount: products.length,
        itemBuilder: (context, index) {
          final targetProduct = products[index];
          return Card(
            //SingleChildScrollView(
            child: Column(
              children: <Widget>[
                SizedBox(
                  height: 300,
                  width: double.infinity,
                  child: Image.asset(
                    targetProduct.image,
                    fit: BoxFit.cover,
                  ),
                ),
                const SizedBox(height: 10),
                Text(
                  '\$${targetProduct.price}',
                  style: const TextStyle(
                    color: Colors.grey,
                    fontSize: 20,
                  ),
                ),
                const SizedBox(
                  height: 10,
                ),
                ElevatedButton(
                    style: ElevatedButton.styleFrom(
                        primary: Colors.blueGrey.shade900),
                    onPressed: () {
                      saveData(Random().nextInt(1000));
                    },
                    child: const Text('Add to Cart')),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 10),
                  width: double.infinity,
                  child: Text(
                    targetProduct.description,
                    textAlign: TextAlign.center,
                    softWrap: true,
                  ),
                ),
              ],
            ),
          );
        })
);

Just think what do you want to show on the list and use the right data. And review why the Product object have a .length method and what it return.

if you want to stop scroll listview add NeverScrollableScrollPhysics

physics: const NeverScrollableScrollPhysics()

Inside ListView widget, use

Related