Flutter GETX, Shopping Cart Example (Getx Obx, Obs)

Flutter is most popular infant in the mobile application and cross platform application development world. Flutter is small in age but very big and vast in term of features, and this is the big reason every mobile application developer is adopting flutter development. Flutter have very large number of libraries and plugins available to use, and with the help of these plugins flutter development become very fast and easy. The most popular package or plugin these days is Flutter Getx. Getx is multiple purpose plugins have lot of tools inside it, but the most demanding and useable feature is state management.

State management is the soul of every application and we can think to make any feature without managing the state of our widgets. In this tutorial we are not going to cover definition or introduction of Getx, if you want to read more about it you can click on this this link and if sitll looking for more simple and practical introduction to understand it more deeply leave your comments down, so that i can make a new post specifically on the introduction of Flutter Getx.

In this tutorial we will learn about how to make shopping cart functionality in flutter using Getx. Shopping app is very common idea in apps, and often we have client who want to develop app for his business and want to sell his product online via app. We will cover basic function of shopping cart as given below and all these screen will be using Getx as state management. Flutter Getx is very simple to use and this flutter Getx shopping app example will give you idea about how we can implement as per our requirements.

  • Product List Screen
  • Product Detail Screen
  • Add product to cart
  • Remover product from cart
  • Add to favourite
  • Remove from favourite
  • Cart Items List

As you can see there are lot of screens in this tutorial so as result this tutorial can be long in comparison with other tutorials.

Please Note- In this tutorial i am not going through the basics of Getx, not covering how to define variables, how to updating the values because I am considering as you are already know about these things.

What we will get in output?

Required Dependencies

get: 
http:
google_fonts:
 

Project Structure

Let’s start step by step implementation so that we can understand what is need to be done to achieve the desired output. First we will convert our whole app which MaterialApp to GetMatrialApp so that we can use Getx as global context in our app.

return GetMaterialApp(

      initialBinding: ControllerBinding(),

      title: 'Getx Shopping Cart Example',
      theme: ThemeData(
        primarySwatch: Colors.red,
      ),
      home: HomeScreen(),
    );

Second we will make Getx Controller class, in this class we will make following variables and functions

Variables

var isWorking = false.obs;
var productsList = [].obs;
var products = {}.obs;
var favList = [].obs;
var isFav = false.obs;
var isAddedToCart = false.obs;
var selectedProduct = {};
var cartProducts = [].obs;

Special Tip

You can declare flutter Getx variable in different ways for example look at this,

RxBool isWorking=false.obs;
in other way
var isWorking=false.obs;

and this is same for other datatypes too.

Functions

getProducts()
getProductDetail(id)
addRemoveToFavorite()
checkFavorite()
addToCart()
removeFromCart(product)
checkCart()

Let’s understand all functions step by step and see what we have done to make the process simple and clean. because when our app grows then we have to take care about the structure of the app, so that we can easily manage our large classes and functions.

While learning flutter Getx state management we are also going to learn how to make call to Api’s and parse the response. So if you have no or less experience with Api calling and response parsing this simple example will teach you in no time.

Special thanks to dummyjson.com to provide a very fast, simple and useful platform for public api, if you are not aware about what is dummyjson.com then you can check the link and get more knowledge about it. But in simple words, they provide public api to get products and other shopping cart related functionality.

product_controller.dart -> getProducts()

This function is responsible to get product from api, and then parse as json to make it useful in our project.

getProducts() async {
    //you can set value in this way too
    //isWorking(true);

    isWorking.value = true;
    const productUrl = 'https://dummyjson.com/products/';

    try {
      var response = await http.get(Uri.parse(productUrl));

      final data = json.decode(response.body);
      productsList.value = data['products'];
    } catch (e) {
      productsList.value = [];

      showSnackBar(title: 'Error', message: 'Failed to get products');
    } finally {
      isWorking.value = false;
    }

    return productsList;
  }

product_controller.dart -> getProductDetail(id)

Now when user will tap on any product we have to open the new screen of product detail, here is the code

getProductDetail(id) async {
    //you can set value in this way too
    //isWorking(true);

    isWorking.value = true;

    final productUrl = 'https://dummyjson.com/products/$id';

    try {
      var response = await http.get(Uri.parse(productUrl));

      final data = json.decode(response.body);
      products.value = data;
    } catch (e) {
      products.value = {};

      showSnackBar(title: 'Error', message: 'Failed to get products');
    } finally {
      isWorking.value = false;
    }
    return products;
  }

product_controller -> addRemoveToFavorite()

In this function we are adding or removing in our favourite list, mostly this will save in local db or live db with the help of your live api, but for now we are storing it in local variable for learning purposes. In coming tutorials we will learn about local database like SQLite, HIve, Sembast and other SQL or NoSql databases. For now let’s move to our example.

As you can see we are not passing any product id to add or remove, this is because we have a variable favList in our controller which is accessible everywhere we are using our controller, so no need to pass any id. When user will tap on any product it will save selected product in a variable, which can be used in different ways as we need.

addRemoveToFavorite() async {
    if (favList.contains(selectedProduct['id'])) {
      favList.remove(selectedProduct['id']);
      isFav.value = false;
    } else {
      favList.add(selectedProduct['id']);
      isFav.value = true;
    }
  }

As i have explained the functions which have the core functionality so next functions i am not going to explain, they are self explanatory. If you have any doubt you can write in comments, I will help to resolve.

product_controller -> checkFavorite()

checkFavorite() {
    if (favList.contains(selectedProduct['id'])) {
      isFav.value = true;
    } else {
      isFav.value = false;
    }
  }

product_controller -> addToCart()

addToCart() {
  if (!cartProducts.contains(selectedProduct)) {
    cartProducts.add(selectedProduct);
    isAddedToCart.value = true;
  } else {
    showSnackBar(title: 'Opps', message: 'Product Already In Cart');
  }
}

product_controller -> removeFromCart(product)

removeFromCart(product) {
  if (cartProducts.contains(product)) {
    cartProducts.remove(product);
  }
}

Here is the complete controller code

product_controller.dart

import 'dart:convert';

import 'package:get/get.dart';
import 'package:http/http.dart' as http;

import '../custom_widgets.dart';

class ProductController extends GetxController {
  //you can declare variable in this way too
  //RxBool isWorking=false.obs;

  var isWorking = false.obs;
  var productsList = [].obs;
  var products = {}.obs;
  var favList = [].obs;
  var isFav = false.obs;
  var isAddedToCart = false.obs;
  var selectedProduct = {};
  var cartProducts = [].obs;

  getProducts() async {
    //you can set value in this way too
    //isWorking(true);

    isWorking.value = true;
    const productUrl = 'https://dummyjson.com/products/';

    try {
      var response = await http.get(Uri.parse(productUrl));

      final data = json.decode(response.body);
      productsList.value = data['products'];
    } catch (e) {
      productsList.value = [];

      showSnackBar(title: 'Error', message: 'Failed to get products');
    } finally {
      isWorking.value = false;
    }

    return productsList;
  }

  getProductDetail(id) async {
    //you can set value in this way too
    //isWorking(true);

    isWorking.value = true;

    final productUrl = 'https://dummyjson.com/products/$id';

    try {
      var response = await http.get(Uri.parse(productUrl));

      final data = json.decode(response.body);
      products.value = data;
    } catch (e) {
      products.value = {};

      showSnackBar(title: 'Error', message: 'Failed to get products');
    } finally {
      isWorking.value = false;
    }
    return products;
  }

  addRemoveToFavorite() async {
    if (favList.contains(selectedProduct['id'])) {
      favList.remove(selectedProduct['id']);
      isFav.value = false;
    } else {
      favList.add(selectedProduct['id']);
      isFav.value = true;
    }
  }

  checkFavorite() {
    if (favList.contains(selectedProduct['id'])) {
      isFav.value = true;
    } else {
      isFav.value = false;
    }
  }

  addToCart() async {
    if (!cartProducts.contains(selectedProduct)) {
      cartProducts.add(selectedProduct);
      isAddedToCart.value = true;
    } else {
      showSnackBar(title: 'Opps', message: 'Product Already In Cart');
    }
  }

  removeFromCart(product) async {
    if (cartProducts.contains(product)) {
      cartProducts.remove(product);
    }
  }

  checkCart() {
    if (cartProducts.contains(selectedProduct)) {
      isAddedToCart.value = true;
    } else {
      isAddedToCart.value = false;
    }
  }
}

Getx Binding

When we create a Getx controller then we need to call it, there are two ways of calling, 1 is with the help of binding and 2 is without binding. But in our tutorial we are using bindings so that we can use full power of Getx, and can do task in proper way.

controller_binding.dart

import 'package:get/get.dart';
import 'package:getx_internet_connectivity/getx/connection_manager_controller.dart';

class ControllerBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<ProductController>(
        () => ProductController());
  }
}

If you want to learn more about flutter development, you can check other tutorials

Now we will call this binding on app loading in memory, it means when we will open our then firstly main method will call runapp function which will call our GetMaterialApp widget. In this GetMaterialApp widget we will call our binding, check the example given below.

main.dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'getx/controller_binding.dart';
import 'screens/home_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialBinding: ControllerBinding(),
      debugShowCheckedModeBanner: false,
      title: 'Shopping Cart Example (Using Getx)',
      theme: ThemeData(
        primarySwatch: Colors.red,
      ),
      home: const HomeScreen(),
    );
  }
}

Before moving to main functionality I need to talk about helper.dart class, this is class is playing very important role in our app’s design. In this class we have made some custom widgets, which will give a special look to our widgets, like custom text view, with easy fonts implementation ans some other user widgets like cart icon etc. Here is the complete class and if you want to more custom widget examples you can check the links below

custom_widgets.dart

import 'package:flutter/material.dart';
import 'package:flutter_getx_shoping_cart/getx/product_controller.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';

import 'screens/list_cart_products.dart';

showSnackBar(
    {required title,
    required message,
    Duration? duration,
    position: SnackPosition.BOTTOM}) {
  Get.snackbar(title, message,
      backgroundColor: Colors.redAccent,
      colorText: Colors.white,
      snackPosition: position,
      duration: duration ?? const Duration(seconds: 3));
}

Widget textView(String text,
    {double fontSize = 14,
    var color = Colors.black87,
    FontWeight fontWeight = FontWeight.normal,
    FontStyle fontStyle = FontStyle.normal,
    Alignment alignment = Alignment.centerLeft,
    int maxLines = 2,
    bool multiLine = true,
    var textOverflow = TextOverflow.ellipsis,
    var textAlign = TextAlign.left,
    var textDirection = TextDirection.ltr,
    var height = 1.2,
    var wordSpacing = 1.0,
    var letterSpacing = 1.0,
    bool needLineThrough = false,
    bool needUnderLine = false,
    needFancyFont = false}) {
  return Align(
    child: Text(
      text,
      style: needFancyFont
          ? GoogleFonts.fjallaOne(
              decoration: needLineThrough
                  ? TextDecoration.lineThrough
                  : needUnderLine
                      ? TextDecoration.underline
                      : TextDecoration.none,
              color: color,
              wordSpacing: wordSpacing,
              letterSpacing: letterSpacing,
              fontWeight: fontWeight,
              fontSize: fontSize,
              fontStyle: fontStyle,
              height: height)
          : GoogleFonts.fjallaOne(
              decoration: needLineThrough
                  ? TextDecoration.lineThrough
                  : needUnderLine
                      ? TextDecoration.underline
                      : TextDecoration.none,
              color: color,
              fontWeight: fontWeight,
              fontSize: fontSize,
              height: height),
      overflow: textOverflow,
      maxLines: maxLines == 0 ? null : maxLines,
//softWrap: true,
      textAlign: textAlign,
      textDirection: textDirection,
    ),
    alignment: alignment,
  );
}

vSpace(height) {
  return SizedBox(
    height: height.toDouble(),
  );
}

hSpace(width) {
  return SizedBox(
    width: width.toDouble(),
  );
}

Widget cartIcon(ProductController productController) {
  return Obx(
    () => Stack(
      children: [
        IconButton(
            onPressed: () {
              Get.to(() => const CartProductsList());
            },
            icon: const Icon(Icons.shopping_cart)),
        if (productController.cartProducts.isNotEmpty)
          Positioned(
            top: 5,
            right: 5,
            child: Container(
              height: 20,
              width: 20,
              decoration: const BoxDecoration(
                  color: Colors.black87, shape: BoxShape.circle),
              child: textView('${productController.cartProducts.length}',
                  color: Colors.white,
                  alignment: Alignment.center,
                  fontSize: 10),
            ),
          )
      ],
    ),
  );
}

Now till here, we are ready with our flutter Getx shopping app core logic, now we are going to implement our logic with our design part, to make app looks like an app.

home_screen.dart

This is main starting class, in this class we will make product list gridview, by getting data from product_controller.dart.

import 'package:flutter/material.dart';
import 'package:flutter_getx_shoping_cart/getx/product_controller.dart';
import 'package:flutter_getx_shoping_cart/custom_widgets.dart';
import 'package:get/get.dart';

import 'ProductDetailScreen.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final ProductController productController = Get.find<ProductController>();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xFFff665e),
        title: const Text('Getx Shopping Store'),
        actions: [
          cartIcon(productController),
        ],
      ),
      body: FutureBuilder<dynamic>(
          future: productController.getProducts(),
          builder: (context, snapshot) {
            if (snapshot.connectionState != ConnectionState.done) {
              return const Center(child: CircularProgressIndicator());
            } else {
              return GridView.builder(
                shrinkWrap: true,
                itemCount: snapshot.data.length,
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2, childAspectRatio: 0.79),
                itemBuilder: (context, index) {
                  final data = snapshot.data.elementAt(index);

                  print(data);
                  return InkWell(
                    onTap: () {
                      productController.selectedProduct = data;

                      Get.to(() => ProductDetailScreen());
                    },
                    child: Stack(
                      children: [
                        Card(
                          margin: const EdgeInsets.all(5),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Container(
                                height: 170,
                                width: Get.width,
                                decoration: BoxDecoration(
                                  image: DecorationImage(
                                      image: NetworkImage(data['thumbnail']),
                                      fit: BoxFit.cover),
                                ),
                              ),
                              Padding(
                                padding:
                                    const EdgeInsets.only(left: 8.0, top: 10),
                                child: textView(data['title']),
                              ),
                              Padding(
                                padding:
                                    const EdgeInsets.only(left: 8.0, top: 5),
                                child: textView("Price \$${data['price']}",
                                    fontWeight: FontWeight.bold),
                              )
                            ],
                          ),
                        ),
                        Obx(
                          () => IconButton(
                            onPressed: () {
                              print('i am fav $index');
                            },
                            icon: productController.favList.contains(data['id'])
                                ? const Icon(Icons.favorite)
                                : const Icon(Icons.favorite_border_outlined),
                          ),
                        )
                      ],
                    ),
                  );
                },
              );
            }
          }),
    );
  }
}

If you have any query feel free to ask. Also, check out the bottom navigation series, custom textfield, custom social login buttons and many others.

Previous Post
Next Post

Leave a Reply

Your email address will not be published. Required fields are marked *