Build a shopping cart with Kotlin - Part 2: Building the shopping cart logic
You will need an Android development environment set up on your machine. Some knowledge of Android development is required.
Introduction
In this second tutorial of this series, we’ll build the shopping cart logic and other things involved in the process. This should allow our users to add products to the cart and know exactly the size of the cart. It’s time to dive into the code.
In the previous tutorial, we’ve built an app that is able to fetch some data from a remote web service and render them using a recycler view to the users.
Prerequisites
To follow along with this tutorial, you will need the following:
- IntelliJ IDEA or Android Studio.
- Experience with the Kotlin language
- Completed part one of the series
- An emulator or physical device (for app testing/debugging purpose)
Build the shopping cart logic
We’ll be building the shopping cart logic and workflow.
First, let’s create a CartItem
entity (a CartItem.kt
as it happens) which represents a single shopping cart item in the ../app/src/main/java/yourPackage
directory:
data class CartItem(var product: Product, var quantity: Int = 0)
As you can see, this class is pretty straightforward, it only has two fields:
- the product to manipulate:
product
, an instance of the Product class we defined in the first part of the tutorial - and the product quantity:
quantity
, just an integer, by default equal to zero
Next, we’ll define a custom class responsible for handling the shopping cart operations such as adding items to the cart, removing items from the cart, getting the cart size and so on.
First, you must add this dependency Paper into your app. It’s a simple but efficient NoSQL-like for Android apps. This library will made things easily for us as we no more need to maintain an SQLite database which is very cumbersome. If you need some info about how to use the Paper library, you can read the documentation here.
Now, head over to your build.gradle
file, then add the following to your dependencies block:
//..app/build.gradle
implementation 'io.paperdb:paperdb:2.6'
Our custom class ShoppingCart
is a wrapper around the Paper library, and contains a set of useful (static) functions for specific purposes. We made use of the Paper library to store our shoppping cart data, and retrieve them whenever we want.
Create a ShoppingCart.kt
file and add the following class:
//..app/src/main/java/yourPackage/ShoppingCart.kt
import android.content.Context
import android.widget.Toast
import io.paperdb.Paper
class ShoppingCart {
companion object {
fun addItem(cartItem: CartItem) {
val cart = ShoppingCart.getCart()
val targetItem = cart.singleOrNull { it.product.id == cartItem.product.id }
if (targetItem == null) {
cartItem.quantity++
cart.add(cartItem)
} else {
targetItem.quantity++
}
ShoppingCart.saveCart(cart)
}
fun removeItem(cartItem: CartItem, context: Context) {
val cart = ShoppingCart.getCart()
val targetItem = cart.singleOrNull { it.product.id == cartItem.product.id }
if (targetItem != null) {
if (targetItem.quantity > 0) {
targetItem.quantity--
} else {
cart.remove(targetItem)
}
}
ShoppingCart.saveCart(cart)
}
fun saveCart(cart: MutableList<CartItem>) {
Paper.book().write("cart", cart)
}
fun getCart(): MutableList<CartItem> {
return Paper.book().read("cart", mutableListOf())
}
fun getShoppingCartSize(): Int {
var cartSize = 0
ShoppingCart.getCart().forEach {
cartSize += it.quantity;
}
return cartSize
}
}
}
We have five dedicated functions in our class:
-
addItem
: this function basically gets acartItem
instance as a parameter, checks first whether it is on the cart or not. If it’s in the cart, we just increment its quantity by one, if this is not the case not only we increment its quantity but we also add it the cart and save the shopping cart to keep it updated. -
removeItem
: here, we also check if thecartItem
instance is present in the cart, then we remove it from the shopping cart if its quantity is equal to zero, but if its quantity is superior to zero we just decreased it by one. Finally, we keep the cart updated by saving it. -
saveCart
: with the help of this function, we can save the state of our app -
getCart
: this function returns our shopping cart itself, basically a list ofCartItem
-
getShoppingCartSize
: helps us get the actual number of items present in our cart
Add remove/add buttons to the design with their listeners
We just added two buttons to our product cart design, one for adding the product to the shopping cart and the second one to remove it. So amend your product_row_item
layout file like the following:
//../app/src/main/res/layout/product_row_item.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardUseCompatPadding="true"
android:layout_margin="2dp"
app:cardBackgroundColor="@android:color/white"
app:cardCornerRadius="4dp"
android:background="?attr/selectableItemBackground"
app:cardElevation="3dp"
android:foreground="?attr/selectableItemBackground"
card_view:cardElevation="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/product_image"
android:layout_width="match_parent"
android:layout_height="180dp"/>
<LinearLayout
android:layout_marginStart="5dp"
android:layout_marginLeft="5dp"
android:padding="10dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textColor="@android:color/black"
android:textSize="22sp"
android:layout_marginBottom="12dp"
android:id="@+id/product_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:textSize="19sp"
android:id="@+id/product_price"
android:textColor="@android:color/holo_red_light"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_gravity="end"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/removeItem"
android:layout_width="wrap_content"
android:paddingHorizontal="16dp"
android:tint="@android:color/white"
android:paddingVertical="4dp"
android:src="@drawable/ic_remove_shopping_cart"
android:background="@color/colorPrimary"
android:layout_height="wrap_content"
card_view:targetApi="o"/>
<ImageButton
android:id="@+id/addToCart"
android:paddingHorizontal="16dp"
android:tint="@android:color/white"
android:paddingVertical="4dp"
android:src="@drawable/ic_add_shopping"
android:background="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
card_view:targetApi="o"/>
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
Here you can find the ic_remove_shopping_cart resource file to paste in ../app/src/main/res/drawable
folder.
Now, head over to your ProductAdapter
file, and add the following inside the view holder inner class in the bindProduct
method body:
../app/src/main/java/yourPackage/ProductAdapter.kt
import android.content.Context
import android.support.design.widget.Snackbar
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.squareup.picasso.Picasso
import io.paperdb.Paper
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.product_row_item.view.*
class ViewHolder(){
......
......
......
fun bindProduct(product:Product){
...
...
...
itemView.addToCart.setOnClickListener { view ->
val item = CartItem(product)
ShoppingCart.addItem(item)
//notify users
Snackbar.make(
(itemView.context as MainActivity).coordinator,
"${product.name} added to your cart",
Snackbar.LENGTH_LONG
).show()
}
itemView.removeItem.setOnClickListener { view ->
val item = CartItem(product)
ShoppingCart.removeItem(item, itemView.context)
Snackbar.make(
(itemView.context as MainActivity).coordinator,
"${product.name} removed from your cart",
Snackbar.LENGTH_LONG
).show()
}
}
}
We just add the listeners to our buttons. When the addToCart
button is clicked, we create a new instance of the CartItem
class is created provided with the product to add, then we add it to the shopping cart with the help of the addItem
utility function we created. Next, we show a snack bar to the user to give them a visual feedback about the success of his action. The process is the same for the removeToCart
click event, except that we removed the item from the shopping cart using our removeItem
utility function.
Add the shopping cart size counter
Now, we want to let our users know our cart size, and keep them updated when an item is added or removed, as you can see in the picture below. Let’s add this button, and the counter to our src/main/res/layout/activity_main.xml
as well as the logic required to make it work as expected. Basically the counter is a text view updated with the shopping cart size.
First, we designed a custom shape to provide a background for our shopping cart counter
../app/src/main/java/res/drawable/item_counter.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@color/colorPrimary"/>
<corners android:radius="50dp"/>
<size android:height="25dp"/>
<size android:width="25dp"/>
</shape>
</item>
</selector>
Add the following after the SwipeRefreshLayout
in the file.
../src/main/java/res/layout/activity_main.xml
...
</android.support.v4.widget.SwipeRefreshLayout>
<RelativeLayout
android:id="@+id/showCart"
android:layout_margin="16dp"
android:layout_gravity="bottom|end"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<android.support.design.widget.FloatingActionButton
android:id="@+id/basketButton"
android:src="@drawable/ic_shopping_basket"
android:tint="@android:color/white"
app:fabSize="normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:padding="8dp"
android:layout_marginBottom="25dp"
android:elevation="50dp"
android:layout_marginStart="5dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:layout_alignRight="@id/basketButton"
android:id="@+id/cart_size"
android:textSize="13sp"
android:background="@drawable/item_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@id/basketButton"
tools:targetApi="lollipop"
android:layout_marginLeft="15dp"/>
</RelativeLayout>
Now, we can refer to our counter with the help of its ID, and provide it with the shopping cart size.
Just add this line to your file in the onCreate
method, before the getProducts
method.
../app/src/main/java/yourPackage/MainActivity.kt
cart_size.text = ShoppingCart.getShoppingCartSize().toString()
Test your app
Now if you run your app and test it, first you should have get the shopping cart shown in the counter. And if you try to add or remove items I guess you get the visual feedback, I mean the snackbar we provided the user with to keep him updated with what it’s going on, but strangely you’re not seeing the counter updated with the actual cart size. You may think that something is going wrong but this is quite normal. Our app is not reactive yet, and we have not even provided it with a means to behave in such an expected way. Well, in the following part we’ll see a more advanced topic, RxKotlin Reactive programming in Kotlin which hopefully will help us add some reactivity to our shopping cart app.
Conclusion
Until now, you’ve learned how to show a list of products to users, built your shopping cart logic enabling your users to add and remove items from your cart.
In the last, part third of this tutorial series we’ll make our mobile app more reactive using a powerful tool: RxKotlin. We’ll also add a shopping cart review functionality to our app to make it more complete 😉.
Nevertheless, you can grab the code for the second part here.
8 March 2019
by Ethiel Adiassa