EXP7
EXP7
No:7
Introduction
In this tutorial I’ll show you how to build a shopping cart using Kotlin. A shopping cart is
a must-have in any e-commerce application. This course will guide you through building
a full shopping cart with Kotlin.
Our end result will be an Android app built with the Kotlin language (rather than Java for
the sake of productivity) listing some products to the user and allowing them to add some
to their shopping cart. They should also be able to review their shopping cart.
In this first part of this tutorial series we’ll be building our app that shows a list of
products to the users.
Demo
Here is the final result of the first part of the tutorial series 😎
Prerequisites
In order to follow along, you will need some experience with the Kotlin programming
language. You will also need appropriate IDEs. I suggest IntelliJ IDEA or Android
Studio. It is also assumed that you know how to use the IDEs that you are working with,
including interacting with either an emulated or physical mobile device for running your
apps.
For this article, we will set the minimum supported Android version at 4.03 (API 15).
Next, choose an empty activity template and click on Finish.
Then head over to your ../app/build.gradle file and paste this inside
the dependencies block, as we’ll be using these dependencies in this tutorial
GRADLE
1//..app/build.gradle
2 implementation 'com.google.code.gson:gson:2.8.2'
3 implementation 'com.squareup.picasso:picasso:2.71828'
4 implementation 'com.squareup.retrofit2:retrofit:2.4.0'
5 implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
6 implementation 'com.squareup.retrofit2:converter-scalars:2.3.0'
7 implementation 'com.android.support:design:28.0.0'
8 implementation 'com.android.support:cardview-v7:28.0.0'
9 implementation 'com.android.support:recyclerview-v7:28.0.0'
Copy
Retrofit: We will need the Retrofit library (a “type-safe HTTP client”) to enable
us send messages to our remote server which we will build later on.
Picasso: Picasso is "A powerful image downloading and caching library for
Android”
Also, amend your styles.xml like the following. This should enable us to use a toolbar
inside our application.
XML
1//..app/src/main/res/values/styles.xml
2
3 <resources>
4
5 <!-- Base application theme. -->
6 <style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
7 <!-- Customize your theme here. -->
8 <item name="colorPrimary">@color/colorPrimary</item>
9 <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
10 <item name="colorAccent">@color/colorAccent</item>
11 </style>
12
13 </resources>
Copy
Our product should have a unique identifier, a price, a name, a description and a set of
images if possible. Now that we know the structure of our product item, let’s define its
model. We’ll build our product entity using a Kotlin data class.
Create a Product.kt file, then copy and paste the following piece of code inside:
KOTLIN
1//..app/src/main/java/yourPackage/Product.kt
2 import com.google.gson.annotations.SerializedName
3
4 data class Product(
5 @SerializedName("description")
6 var description: String? = null,
7
8 @SerializedName("id")
9 var id: Int? = null,
10
11 @SerializedName("name")
12 var name: String? = null,
13
14 @SerializedName("price")
15 var price: String? = null,
16
17 @SerializedName("photos")
18 var photos: List<Photo> = arrayListOf()
19 )
Copy
As our product has a set of photos, we’ll also define its entity. Create a Photo.kt file, then
paste the following code inside as well:
KOTLIN
1//..app/src/main/java/yourPackage/Photo.kt
2 import com.google.gson.annotations.SerializedName
3
4 data class Photo(
5 @SerializedName("filename")
6 var filename: String? = null
7 )
Copy
Next, we’ll build our product adapter responsible to handle the display of our products
list.
KOTLIN
1//..app/src/main/java/yourPackage/ProductAdapter.kt
2
3 import android.annotation.SuppressLint
4 import android.content.Context
5 import android.support.design.widget.Snackbar
6 import android.support.v7.widget.RecyclerView
7 import android.view.LayoutInflater
8 import android.view.View
9 import android.view.ViewGroup
10 import android.widget.Toast
11 import com.squareup.picasso.Picasso
12 import kotlinx.android.synthetic.main.activity_main.*
13 import kotlinx.android.synthetic.main.product_row_item.view.*
14
15 class ProductAdapter(var context: Context, var products: List<Product> =
arrayListOf()) :
16 RecyclerView.Adapter<ProductAdapter.ViewHolder>() {
17 override fun onCreateViewHolder(p0: ViewGroup, p1: Int):
ProductAdapter.ViewHolder {
18 // The layout design used for each list item
19 val view = LayoutInflater.from(context).inflate(R.layout.product_row_item, null)
20 return ViewHolder(view)
21
22 }
23 // This returns the size of the list.
24 override fun getItemCount(): Int = products.size
25
26 override fun onBindViewHolder(viewHolder: ProductAdapter.ViewHolder, position:
Int) {
27 //we simply call the `bindProduct` function here
28 viewHolder.bindProduct(products[position])
29 }
30
31 class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
32
33 // This displays the product information for each item
34 fun bindProduct(product: Product) {
35
36 itemView.product_name.text = product.name
37 itemView.product_price.text = "$${product.price.toString()}"
38
Picasso.get().load(product.photos[0].filename).fit().into(itemView.product_image)
39 }
40
41 }
42
43 }
Copy
Next, let’s create our product item layout. This layout file contains:
All these widgets are wrapped inside a CardView to add a shadow and a radius to the
layout 🙂.
Create a product_row_item file and paste the following inside. This layout is responsible
for handling the view of a single item of our list.
XML
1//../app/src/main/java/res/layout/product_row_item.xml
2
3 <?xml version="1.0" encoding="utf-8"?>
4 <android.support.v7.widget.CardView
5 xmlns:card_view="http://schemas.android.com/tools"
6 xmlns:android="http://schemas.android.com/apk/res/android"
7 xmlns:app="http://schemas.android.com/apk/res-auto"
8 app:cardUseCompatPadding="true"
9 android:layout_margin="4dp"
10 app:cardBackgroundColor="@android:color/white"
11 app:cardCornerRadius="4dp"
12 android:background="?attr/selectableItemBackground"
13 app:cardElevation="3dp"
14 android:foreground="?attr/selectableItemBackground"
15 card_view:cardElevation="4dp"
16 android:layout_width="match_parent"
17 android:layout_height="wrap_content">
18
19 <LinearLayout
20 android:orientation="vertical"
21 android:layout_width="match_parent"
22 android:layout_height="match_parent">
23
24 <ImageView
25 android:id="@+id/product_image"
26 android:layout_width="match_parent"
27 android:layout_height="140dp"/>
28
29 <LinearLayout
30 android:padding="10dp"
31 android:orientation="vertical"
32 android:layout_width="match_parent"
33 android:layout_height="wrap_content">
34
35
36 <TextView
37 android:textColor="@android:color/black"
38 android:textSize="22sp"
39 android:layout_marginBottom="12dp"
40 android:id="@+id/product_name"
41 android:layout_width="wrap_content"
42 android:layout_height="wrap_content"/>
43
44
45 <TextView
46 android:textSize="19sp"
47 android:textColor="@android:color/black"
48 android:id="@+id/product_price"
49 android:layout_width="wrap_content"
50 android:layout_height="wrap_content"/>
51 </LinearLayout>
52
53 <ImageButton
54 android:id="@+id/addToCart"
55 android:paddingHorizontal="16dp"
56 android:tint="@android:color/white"
57 android:paddingVertical="4dp"
58 android:src="@drawable/ic_add_shopping"
59 android:layout_gravity="end"
60 android:background="@color/colorAccent"
61 android:layout_width="wrap_content"
62 android:layout_height="wrap_content"
63 card_view:targetApi="o"/>
64
65 </LinearLayout>
66
67 </android.support.v7.widget.CardView>
Copy
These are the links to get the drawable icons we used in our layout
: ic_add_shopping and ic_shopping_basket. They are to paste
in ../app/src/main/res/drawable folder.
Create an APIConfig.kt file. This class gives us an instance of Retrofit for our network
calls:
KOTLIN
1//..app/src/main/java/yourPackage/APIConfig.kt
2 import android.content.Context
3 import okhttp3.OkHttpClient
4 import retrofit2.Retrofit
5 import retrofit2.converter.gson.GsonConverterFactory
6 import com.google.gson.GsonBuilder
7 import retrofit2.converter.scalars.ScalarsConverterFactory
8
9 object APIConfig {
10
11 val BASE_URL = "https://all-spices.com/api/products/"
12
13 private var retrofit: Retrofit? = null
14
15 var gson = GsonBuilder()
16 .setLenient()
17 .create()
18
19 fun getRetrofitClient(context: Context): Retrofit {
20
21 val okHttpClient = OkHttpClient.Builder()
22 .build()
23
24 if (retrofit == null) {
25 retrofit = Retrofit.Builder()
26 .baseUrl(BASE_URL)
27 .client(okHttpClient)
28 .addConverterFactory(ScalarsConverterFactory.create())
29 .addConverterFactory(GsonConverterFactory.create(gson))
30 .build()
31 }
32 return retrofit!!
33 }
34 }
Copy
KOTLIN
1import retrofit2.Call
2 import retrofit2.http.*
3
4 interface APIService {
5 @Headers("Content-Type: application/json", "Accept: application/json")
6 @GET("bestRated")
7 fun getProducts(
8 ): Call<List<Product>>
9
10 }
Copy
Listing products
For listing products, we’ll need a recycler view (a recycler view is a widget for listing a
list of items, as it happens our products list). Now, move on to
your src/main/java/res/layout/activity_main.xml file, amend it like the following:
XML
1<?xml version="1.0" encoding="utf-8"?>
2 <android.support.design.widget.CoordinatorLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:layout_height="match_parent"
6 android:background="#fffffa"
7 android:id="@+id/coordinator"
8 android:layout_width="match_parent">
9
10
11 <android.support.design.widget.AppBarLayout
12 android:background="@android:color/transparent"
13 android:fitsSystemWindows="true"
14 android:layout_width="match_parent"
15 android:layout_height="?attr/actionBarSize">
16
17 <android.support.v7.widget.Toolbar
18 android:id="@+id/toolbar"
19 app:titleTextColor="@color/colorAccent"
20 app:title="Shopping List"
21 android:background="@android:color/white"
22 android:layout_width="match_parent"
23 android:layout_height="?attr/actionBarSize">
24
25 </android.support.v7.widget.Toolbar>
26
27 </android.support.design.widget.AppBarLayout>
28
29
30 <android.support.v4.widget.SwipeRefreshLayout
31 android:id="@+id/swipeRefreshLayout"
32 app:layout_behavior="@string/appbar_scrolling_view_behavior"
33 android:layout_width="match_parent"
34 android:layout_height="wrap_content">
35
36 <android.support.v7.widget.RecyclerView
37 android:id="@+id/products_recyclerview"
38 android:layout_width="match_parent"
39 android:layout_height="match_parent"/>
40
41 </android.support.v4.widget.SwipeRefreshLayout>
42
43
44 <android.support.design.widget.FloatingActionButton
45 android:id="@+id/showBasket"
46 android:src="@drawable/ic_shopping_basket"
47 android:tint="@android:color/white"
48 android:layout_margin="16dp"
49 android:layout_gravity="bottom|end"
50 app:fabSize="normal"
51 android:layout_width="wrap_content"
52 android:layout_height="wrap_content"/>
53
54 </android.support.design.widget.CoordinatorLayout>
Copy
KOTLIN
1//..app/src/main/java/yourPackage/MainActivity.kt
2
3 import android.content.Intent
4 import android.support.v7.app.AppCompatActivity
5 import android.os.Bundle
6 import android.support.v4.content.ContextCompat
7 import android.support.v7.widget.StaggeredGridLayoutManager
8 import android.util.Log
9 import android.widget.Toast
10 import kotlinx.android.synthetic.main.activity_main.*
11 import retrofit2.Call
12 import retrofit2.Response
13
14 class MainActivity : AppCompatActivity() {
15
16 private lateinit var apiService: APIService
17 private lateinit var productAdapter: ProductAdapter
18
19 private var products = listOf<Product>()
20
21 override fun onCreate(savedInstanceState: Bundle?) {
22 super.onCreate(savedInstanceState)
23
24 setContentView(R.layout.activity_main)
25
26 setSupportActionBar(toolbar)
27 apiService = APIConfig.getRetrofitClient(this).create(APIService::class.java)
28
29 swipeRefreshLayout.setColorSchemeColors(ContextCompat.getColor(this,
R.color.colorPrimary))
30
31 swipeRefreshLayout.isRefreshing = true
32
33 // assign a layout manager to the recycler view
34 products_recyclerview.layoutManager = StaggeredGridLayoutManager(2,
StaggeredGridLayoutManager.VERTICAL)
35
36 getProducts()
37
38 }
39
40
41 fun getProducts() {
42 apiService.getProducts().enqueue(object : retrofit2.Callback<List<Product>> {
43 override fun onFailure(call: Call<List<Product>>, t: Throwable) {
44
45 print(t.message)
46 Log.d("Data error", t.message)
47 Toast.makeText(this@MainActivity, t.message,
Toast.LENGTH_SHORT).show()
48
49 }
50
51 override fun onResponse(call: Call<List<Product>>, response:
Response<List<Product>>) {
52
53 swipeRefreshLayout.isRefreshing = false
54 products = response.body()!!
55
56 productAdapter = ProductAdapter(this@MainActivity, products)
57
58 products_recyclerview.adapter = productAdapter
59 productAdapter.notifyDataSetChanged()
60
61 }
62
63 })
64 }
65
66 }
Copy
In the getProducts methods, we made an API call to fetch our products, if everything gets
well we first disable the swipe refresh layout, then assign the result to our products list,
initialised our product adapter, assigned the adapter to the recycler view, and tell the
adapter data its state has changed. Otherwise, we just logged the error for debugging
purpose.
Next up is to add the Internet permission in your AndroidManifest.xml file. Update the
file with the code snippet below:
XML
1//app/src/main
2 <uses-permission android:name="android.permission.INTERNET"/>
Copy
We are done with the first part of this article. Now you can run your app to see if
everything is correct.
As you can see, this class is pretty straightforward, it only has two fields:
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:
GRADLE
1//..app/build.gradle
2 implementation 'io.paperdb:paperdb:2.6'
Copy
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.
KOTLIN
1//..app/src/main/java/yourPackage/ShoppingCart.kt
2 import android.content.Context
3 import android.widget.Toast
4 import io.paperdb.Paper
5
6 class ShoppingCart {
7
8 companion object {
9 fun addItem(cartItem: CartItem) {
10 val cart = ShoppingCart.getCart()
11
12 val targetItem = cart.singleOrNull { it.product.id == cartItem.product.id }
13 if (targetItem == null) {
14 cartItem.quantity++
15 cart.add(cartItem)
16 } else {
17 targetItem.quantity++
18 }
19 ShoppingCart.saveCart(cart)
20 }
21
22 fun removeItem(cartItem: CartItem, context: Context) {
23 val cart = ShoppingCart.getCart()
24
25 val targetItem = cart.singleOrNull { it.product.id == cartItem.product.id }
26 if (targetItem != null) {
27 if (targetItem.quantity > 0) {
28 targetItem.quantity--
29 } else {
30 cart.remove(targetItem)
31 }
32 }
33
34 ShoppingCart.saveCart(cart)
35 }
36
37 fun saveCart(cart: MutableList<CartItem>) {
38 Paper.book().write("cart", cart)
39 }
40
41 fun getCart(): MutableList<CartItem> {
42 return Paper.book().read("cart", mutableListOf())
43 }
44
45 fun getShoppingCartSize(): Int {
46 var cartSize = 0
47 ShoppingCart.getCart().forEach {
48 cartSize += it.quantity;
49 }
50
51 return cartSize
52 }
53 }
54
55 }
Copy
XML
1//../app/src/main/res/layout/product_row_item.xml
2
3 <?xml version="1.0" encoding="utf-8"?>
4 <android.support.v7.widget.CardView
5 xmlns:card_view="http://schemas.android.com/tools"
6 xmlns:android="http://schemas.android.com/apk/res/android"
7 xmlns:app="http://schemas.android.com/apk/res-auto"
8 app:cardUseCompatPadding="true"
9 android:layout_margin="2dp"
10 app:cardBackgroundColor="@android:color/white"
11 app:cardCornerRadius="4dp"
12 android:background="?attr/selectableItemBackground"
13 app:cardElevation="3dp"
14 android:foreground="?attr/selectableItemBackground"
15 card_view:cardElevation="4dp"
16 android:layout_width="match_parent"
17 android:layout_height="wrap_content">
18
19 <LinearLayout
20 android:orientation="vertical"
21 android:layout_width="match_parent"
22 android:layout_height="match_parent">
23
24 <ImageView
25 android:id="@+id/product_image"
26 android:layout_width="match_parent"
27 android:layout_height="180dp"/>
28
29 <LinearLayout
30 android:layout_marginStart="5dp"
31 android:layout_marginLeft="5dp"
32 android:padding="10dp"
33 android:orientation="vertical"
34 android:layout_width="match_parent"
35 android:layout_height="wrap_content">
36
37
38 <TextView
39 android:textColor="@android:color/black"
40 android:textSize="22sp"
41 android:layout_marginBottom="12dp"
42 android:id="@+id/product_name"
43 android:layout_width="wrap_content"
44 android:layout_height="wrap_content"/>
45
46
47 <TextView
48 android:textSize="19sp"
49 android:id="@+id/product_price"
50 android:textColor="@android:color/holo_red_light"
51 android:layout_width="wrap_content"
52 android:layout_height="wrap_content"/>
53 </LinearLayout>
54
55
56 <LinearLayout
57 android:layout_gravity="end"
58 android:orientation="horizontal"
59 android:layout_width="wrap_content"
60 android:layout_height="wrap_content">
61
62 <ImageButton
63 android:id="@+id/removeItem"
64 android:layout_width="wrap_content"
65 android:paddingHorizontal="16dp"
66 android:tint="@android:color/white"
67 android:paddingVertical="4dp"
68 android:src="@drawable/ic_remove_shopping_cart"
69 android:background="@color/colorPrimary"
70 android:layout_height="wrap_content"
71 card_view:targetApi="o"/>
72
73 <ImageButton
74 android:id="@+id/addToCart"
75 android:paddingHorizontal="16dp"
76 android:tint="@android:color/white"
77 android:paddingVertical="4dp"
78 android:src="@drawable/ic_add_shopping"
79 android:background="@color/colorAccent"
80 android:layout_width="wrap_content"
81 android:layout_height="wrap_content"
82 card_view:targetApi="o"/>
83
84 </LinearLayout>
85
86
87 </LinearLayout>
88
89 </android.support.v7.widget.CardView>
Copy
Now, head over to your ProductAdapter file, and add the following inside the view
holder inner class in the bindProduct method body:
KOTLIN
1../app/src/main/java/yourPackage/ProductAdapter.kt
2
3 import android.content.Context
4 import android.support.design.widget.Snackbar
5 import android.support.v7.widget.RecyclerView
6 import android.view.LayoutInflater
7 import android.view.View
8 import android.view.ViewGroup
9 import android.widget.Toast
10 import com.squareup.picasso.Picasso
11 import io.paperdb.Paper
12 import kotlinx.android.synthetic.main.activity_main.*
13 import kotlinx.android.synthetic.main.product_row_item.view.*
14
15 class ViewHolder(){
16
17 ......
18 ......
19 ......
20
21
22 fun bindProduct(product:Product){
23 ...
24 ...
25 ...
26
27 itemView.addToCart.setOnClickListener { view ->
28
29 val item = CartItem(product)
30
31 ShoppingCart.addItem(item)
32 //notify users
33 Snackbar.make(
34 (itemView.context as MainActivity).coordinator,
35 "${product.name} added to your cart",
36 Snackbar.LENGTH_LONG
37 ).show()
38
39 }
40
41 itemView.removeItem.setOnClickListener { view ->
42
43 val item = CartItem(product)
44
45 ShoppingCart.removeItem(item, itemView.context)
46
47 Snackbar.make(
48 (itemView.context as MainActivity).coordinator,
49 "${product.name} removed from your cart",
50 Snackbar.LENGTH_LONG
51 ).show()
52
53 }
54
55 }
56
57 }
Copy
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
XML
1../app/src/main/java/res/drawable/item_counter.xml
2
3 <?xml version="1.0" encoding="utf-8"?>
4 <selector xmlns:android="http://schemas.android.com/apk/res/android">
5 <item>
6 <shape android:shape="oval">
7 <solid android:color="@color/colorPrimary"/>
8 <corners android:radius="50dp"/>
9 <size android:height="25dp"/>
10 <size android:width="25dp"/>
11 </shape>
12 </item>
13 </selector>
Copy
XML
1../src/main/java/res/layout/activity_main.xml
2 ...
3 </android.support.v4.widget.SwipeRefreshLayout>
4
5 <RelativeLayout
6 android:id="@+id/showCart"
7 android:layout_margin="16dp"
8 android:layout_gravity="bottom|end"
9 android:layout_width="wrap_content"
10 android:layout_height="wrap_content">
11
12 <android.support.design.widget.FloatingActionButton
13 android:id="@+id/basketButton"
14 android:src="@drawable/ic_shopping_basket"
15 android:tint="@android:color/white"
16 app:fabSize="normal"
17 android:layout_width="wrap_content"
18 android:layout_height="wrap_content"/>
19
20 <TextView
21 android:padding="8dp"
22 android:layout_marginBottom="25dp"
23 android:elevation="50dp"
24 android:layout_marginStart="5dp"
25 android:textColor="@android:color/white"
26 android:textStyle="bold"
27 android:layout_alignRight="@id/basketButton"
28 android:id="@+id/cart_size"
29 android:textSize="13sp"
30 android:background="@drawable/item_counter"
31 android:layout_width="wrap_content"
32 android:layout_height="wrap_content"
33 android:layout_alignEnd="@id/basketButton"
34 tools:targetApi="lollipop"
35 android:layout_marginLeft="15dp"/>
36
37 </RelativeLayout>
Copy
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.
KOTLIN
1../app/src/main/java/yourPackage/MainActivity.kt
2
3 cart_size.text = ShoppingCart.getShoppingCartSize().toString()
Copy