Skip to main content

Building a Dynamic Product List with GetX in Flutter

Building a Dynamic Product List with GetX in Flutter

Learn how to create a responsive product screen using GetX in Flutter. This tutorial covers API integration, pagination, cart management, and quantity selection with efficient state management. Perfect for building an e-commerce app!


class ProductScreenController extends GetxController{

  

  RxList<Product> productList = <Product>[].obs;

  RxList<Product> cartList = <Product>[].obs;

  RxMap<int , int> quantityMap = <int , int>{}.obs;

  RxMap<int, bool> showQuantitySelector = <int, bool>{}.obs; // ✅ Track Visibility of Quantity Selector


  RxBool isLoading = false.obs;

  RxBool isLoadingMore = false.obs;

  final Dio dio = Dio();

  int limit = 10;

  int skip = 0;

  bool hasMore = true; // ✅ Track if more data is available


  @override

  void onInit() {  // ✅ Automatically Call API when Controller initializes

    super.onInit();

    fetchData();

  }




  Future<void> fetchData({bool isPagination = false}) async {

    if (!hasMore) return;


    try {

      if (!isPagination) {

        isLoading.value = true;

      } else {

        isLoadingMore.value = true; // ✅ Show loading indicator at bottom

      }


      final response = await dio.get(

        'https://dummyjson.com/products',

        queryParameters: {

          'limit': limit,

          'skip': skip,

        },

      );


      if (response.statusCode == 200) {

        ProductScreenModel productScreenModel = ProductScreenModel.fromMap(response.data);


        if (productScreenModel.products.isEmpty) {

          hasMore = false; // ✅ No more products available

        } else {

          productList.addAll(productScreenModel.products); // ✅ Add new data

          skip += limit; // ✅ Update skip for next page

        }

      } else {

        Get.snackbar("Error", "Data not Found");

      }

    } catch (e) {

      Get.snackbar("Error", "Error Found: $e");

    } finally {

      isLoading.value = false;

      isLoadingMore.value = false;

    }

  }


  // ✅ Toggle Quantity Selector Visibility

  void toggleQuantitySelector(int productId) {

    showQuantitySelector[productId] = !(showQuantitySelector[productId] ?? false);

    if (showQuantitySelector[productId] == true) {

      addToCart(productList.firstWhere((p) => p.id == productId));

    }

  }


  void addToCart(Product product){

    cartList.add(product);

    if(cartList.contains(product)){

      quantityMap[product.id] = 1;

    }else{

      quantityMap[product.id] = (quantityMap[product.id] ?? 0) + 1;

    }

    Get.snackbar("Added to Cart", "${product.title} added successfully");

  }


  void removeFromCart(Product product){

    cartList.remove(product);

    quantityMap.remove(product.id);

    Get.snackbar("Added to Cart", "${product.title} added successfully");

    showQuantitySelector[product.id] = false; // Hide selector on removal

    Get.snackbar("Removed", "${product.title} removed from cart!");

  }


  void increaseQuantity(int productId){

    if(quantityMap.containsKey(productId)){

      quantityMap[productId] = quantityMap[productId]! + 1;

    }

  }


  void decreaseQuantity(int productId){

    if(quantityMap.containsKey(productId) && quantityMap[productId]! > 1){

      quantityMap[productId] = quantityMap[productId]! - 1;

    }else{

      removeFromCart(productList.firstWhere((p) => p.id == productId));

      showQuantitySelector[productId] = false;

    }

  }

}


You'll learn how to manage UI states, handle errors gracefully, and implement smooth user interactions. By the end, you'll have a functional product listing with add-to-cart functionality—perfect for e-commerce applications

First screen

class ProductScreen extends StatefulWidget { // ✅ Ensure Correct StatefulWidget
  const ProductScreen({Key? key}) : super(key: key);

  @override
  _ProductScreenState createState() => _ProductScreenState(); // ✅ Proper State Initialization
}

class _ProductScreenState extends State<ProductScreen> {
  final ProductScreenController controller = Get.put(ProductScreenController());
  final ScrollController scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    scrollController.addListener(() {
      if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
        controller.fetchData(isPagination: true);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Products'),
        centerTitle: true,
        actions: [
          IconButton(
            icon: Icon(Icons.shopping_cart),
            onPressed: () {
              Get.to(() => CartScreen());
            },
          ),
        ],
      ),
      body: Obx(() {
        if (controller.isLoading.value) {
          return Center(child: CircularProgressIndicator());
        }
        if (controller.productList.isEmpty) {
          return Center(child: Text('No Products Found!'));
        }
        return ListView.builder(
          controller: scrollController,
          itemCount: controller.productList.length + 1,
          itemBuilder: (context, index) {
            if (index == controller.productList.length) {
              return controller.isLoadingMore.value
                  ? Padding(
                padding: EdgeInsets.all(10),
                child: Center(child: CircularProgressIndicator()),
              )
                  : SizedBox();
            }

            var product = controller.productList[index];
            return Card(
              margin: EdgeInsets.all(10),
              child: ListTile(
                leading: Image.network(product.thumbnail, width: 50, height: 50, fit: BoxFit.cover),
                title: Text(product.title, style: TextStyle(fontWeight: FontWeight.bold)),
                subtitle: Text("Price: ₹${product.price}"),
                trailing: Obx(() {
                  bool showQuantity = controller.showQuantitySelector[product.id] ?? false;
                  return showQuantity
                      ? Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                        icon: Icon(Icons.remove, color: Colors.red),
                        onPressed: () {
                          controller.decreaseQuantity(product.id);
                        },
                      ),
                      Text("${controller.quantityMap[product.id] ?? 1}"),
                      IconButton(
                        icon: Icon(Icons.add, color: Colors.green),
                        onPressed: () {
                          controller.increaseQuantity(product.id);
                        },
                      ),
                    ],
                  )
                      : IconButton(
                    icon: Icon(Icons.shopping_cart, color: Colors.blue),
                    onPressed: () {
                      controller.toggleQuantitySelector(product.id);
                    },
                  );
                }),
              ),
            );
          },
        );
      }),
    );
  }
}


Cart screen


class CartScreen extends StatelessWidget {

  final ProductScreenController controller = Get.find<ProductScreenController>();


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text('Cart'),

        centerTitle: true,

      ),

      body: Obx(() {

        if (controller.cartList.isEmpty) {

          return Center(child: Text('Your Cart is Empty!'));

        }

        return ListView.builder(

          itemCount: controller.cartList.length,

          itemBuilder: (context, index) {

            var product = controller.cartList[index];

            return Card(

              margin: EdgeInsets.all(10),

              child: ListTile(

                leading: Image.network(product.thumbnail, width: 50, height: 50, fit: BoxFit.cover),

                title: Text(product.title, style: TextStyle(fontWeight: FontWeight.bold)),

                subtitle: Text("Price: ₹${product.price}"),

                trailing:Obx((){

                  return Row(

                    mainAxisSize: MainAxisSize.min,

                    children: [

                      IconButton(

                        icon: Icon(Icons.remove, color: Colors.red),

                        onPressed: () {

                          controller.decreaseQuantity(product.id);

                        },

                      ),

                      Text("${controller.quantityMap[product.id] ?? 1}"),

                      IconButton(

                        icon: Icon(Icons.add, color: Colors.green),

                        onPressed: () {

                          controller.increaseQuantity(product.id);

                        },

                      ),

                    ],

                  );

                })

              ),

            );

          },

        );

      }),

    );

  }

}

Model

ProductScreenModel productScreenModelFromMap(String str) => ProductScreenModel.fromMap(json.decode(str));


String productScreenModelToMap(ProductScreenModel data) => json.encode(data.toMap());


class ProductScreenModel {

  List<Product> products;

  int total;

  int skip;

  int limit;


  ProductScreenModel({

    required this.products,

    required this.total,

    required this.skip,

    required this.limit,

  });


  factory ProductScreenModel.fromMap(Map<String, dynamic> json) => ProductScreenModel(

    products: List<Product>.from(json["products"].map((x) => Product.fromMap(x))),

    total: json["total"],

    skip: json["skip"],

    limit: json["limit"],

  );


  Map<String, dynamic> toMap() => {

    "products": List<dynamic>.from(products.map((x) => x.toMap())),

    "total": total,

    "skip": skip,

    "limit": limit,

  };

}


class Product {

  int id;

  String title;

  String description;

  String category;

  double price;

  double discountPercentage;

  double rating;

  int stock;

  List<String> tags;

  String brand;

  String sku;

  int weight;

  Dimensions dimensions;

  String warrantyInformation;

  String shippingInformation;

  String availabilityStatus;

  List<Review> reviews;

  String returnPolicy;

  int minimumOrderQuantity;

  Meta meta;

  String thumbnail;

  List<String> images;


  Product({

    required this.id,

    required this.title,

    required this.description,

    required this.category,

    required this.price,

    required this.discountPercentage,

    required this.rating,

    required this.stock,

    required this.tags,

    required this.brand,

    required this.sku,

    required this.weight,

    required this.dimensions,

    required this.warrantyInformation,

    required this.shippingInformation,

    required this.availabilityStatus,

    required this.reviews,

    required this.returnPolicy,

    required this.minimumOrderQuantity,

    required this.meta,

    required this.thumbnail,

    required this.images,

  });


  factory Product.fromMap(Map<String, dynamic> json) => Product(

    id: json["id"] ?? 0,

    title: json["title"] ?? "",

    description: json["description"] ?? "",

    category: json["category"]?? "",

    price: json["price"]?.toDouble()?? 0,

    discountPercentage: json["discountPercentage"]?.toDouble()?? 0,

    rating: json["rating"]?.toDouble() ?? 0,

    stock: json["stock"] ?? "",

    tags: List<String>.from(json["tags"].map((x) => x)),

    brand: json["brand"]?? "",

    sku: json["sku"],

    weight: json["weight"],

    dimensions: Dimensions.fromMap(json["dimensions"]),

    warrantyInformation: json["warrantyInformation"],

    shippingInformation: json["shippingInformation"],

    availabilityStatus: json["availabilityStatus"],

    reviews: List<Review>.from(json["reviews"].map((x) => Review.fromMap(x))),

    returnPolicy: json["returnPolicy"],

    minimumOrderQuantity: json["minimumOrderQuantity"],

    meta: Meta.fromMap(json["meta"]),

    thumbnail: json["thumbnail"],

    images: List<String>.from(json["images"].map((x) => x)),

  );


  Map<String, dynamic> toMap() => {

    "id": id,

    "title": title,

    "description": description,

    "category": category,

    "price": price,

    "discountPercentage": discountPercentage,

    "rating": rating,

    "stock": stock,

    "tags": List<dynamic>.from(tags.map((x) => x)),

    "brand": brand,

    "sku": sku,

    "weight": weight,

    "dimensions": dimensions.toMap(),

    "warrantyInformation": warrantyInformation,

    "shippingInformation": shippingInformation,

    "availabilityStatus": availabilityStatus,

    "reviews": List<dynamic>.from(reviews.map((x) => x.toMap())),

    "returnPolicy": returnPolicy,

    "minimumOrderQuantity": minimumOrderQuantity,

    "meta": meta.toMap(),

    "thumbnail": thumbnail,

    "images": List<dynamic>.from(images.map((x) => x)),

  };

}


class Dimensions {

  double width;

  double height;

  double depth;


  Dimensions({

    required this.width,

    required this.height,

    required this.depth,

  });


  factory Dimensions.fromMap(Map<String, dynamic> json) => Dimensions(

    width: json["width"]?.toDouble(),

    height: json["height"]?.toDouble(),

    depth: json["depth"]?.toDouble(),

  );


  Map<String, dynamic> toMap() => {

    "width": width,

    "height": height,

    "depth": depth,

  };

}


class Meta {

  DateTime createdAt;

  DateTime updatedAt;

  String barcode;

  String qrCode;


  Meta({

    required this.createdAt,

    required this.updatedAt,

    required this.barcode,

    required this.qrCode,

  });


  factory Meta.fromMap(Map<String, dynamic> json) => Meta(

    createdAt: DateTime.parse(json["createdAt"]),

    updatedAt: DateTime.parse(json["updatedAt"]),

    barcode: json["barcode"],

    qrCode: json["qrCode"],

  );


  Map<String, dynamic> toMap() => {

    "createdAt": createdAt.toIso8601String(),

    "updatedAt": updatedAt.toIso8601String(),

    "barcode": barcode,

    "qrCode": qrCode,

  };

}


class Review {

  int rating;

  String comment;

  DateTime date;

  String reviewerName;

  String reviewerEmail;


  Review({

    required this.rating,

    required this.comment,

    required this.date,

    required this.reviewerName,

    required this.reviewerEmail,

  });


  factory Review.fromMap(Map<String, dynamic> json) => Review(

    rating: json["rating"],

    comment: json["comment"],

    date: DateTime.parse(json["date"]),

    reviewerName: json["reviewerName"],

    reviewerEmail: json["reviewerEmail"],

  );


  Map<String, dynamic> toMap() => {

    "rating": rating,

    "comment": comment,

    "date": date.toIso8601String(),

    "reviewerName": reviewerName,

    "reviewerEmail": reviewerEmail,

  };

}



Comments

Popular posts from this blog

Flutter pagination tutorial

option 1 import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; // API Service class ApiService {   static const String baseUrl = "https://jsonplaceholder.typicode.com";   static Future<List<dynamic>> fetchData(int page, int limit) async {     final response = await http.get(Uri.parse("$baseUrl/posts?_page=$page&_limit=$limit"));     if (response.statusCode == 200) {       return json.decode(response.body);     } else {       throw Exception("Failed to load data");     }   } } // GetX Controller for Pagination class PaginationController extends GetxController {   var items = <dynamic>[].obs;   var page = 1.obs;   final int limit = 10;   var isLoading = false.obs;   var hasMore = true.obs;   ScrollController scrollController = ScrollController();   @override   void...

send data controller to provider

 send data controller to provider import 'package:flutter/material.dart'; import 'model.dart'; class TaskProvider extends ChangeNotifier {   List<Task> _tasks = [];   List<Task> get tasks => _tasks;   // Create operation   void addTask(Task task) {     _tasks.add(task);     notifyListeners();   }   // Read operation (already accessible via getter tasks)   // Update operation   void updateTask(Task task) {     // Find the task in the list and update it     int index = _tasks.indexWhere((t) => t.id == task.id);     if (index != -1) {       _tasks[index] = task;       notifyListeners();     }   }   // Delete operation   void deleteTask(String id) {     _tasks.removeWhere((task) => task.id == id);     notifyListeners();   } } class Task {   String id;   String title;   bool completed...