Build a shopping cart with Kotlin - Part 3: Adding reactivity to the app
You will need an Android development environment set up on your machine. Some knowledge of Android development is required.
Introduction
In this last part of the tutorial series, we will add some reactivity to our app using RxKotlin, and complete our shopping cart.
Prerequisites
In order to follow along with the tutorial, make sure you meet the following requirements:
- IntelliJ IDEA or Android Studio.
- Experience with the Kotlin language
- Having followed and completed the previous parts of the tutorial series: Part 1 Part 2
- An emulator or physical device (for app testing/debugging purpose)
What’s RxKotlin?
You may not know what RxKotlin is about. Well I cannot blame you for that 🙂 . The first thing you should know is that RxKotlin is about Reactive programming with Kotlin. And you may also ask what is reactive programming, right ?
According to Wikipedia,
Reactive programming is a programming paradigm oriented around data flows and the propagation of change. This means that it should be possible to express static or dynamic data flows with ease in the programming languages used, and that the underlying execution model will automatically propagate changes through the data flow
This paradigm is built on the ideology of the observable pattern, and provides us with many tools to build reactive apps. We have essentially three components involved in reactive programming:
-
Observers: an observer subscribes to an observable.
-
Observables: an observable emits a stream of data which the the observer listens for and reacts to.
-
Operators: they allow the observer to perform a set of really useful operations over the sequence of items emitted by the the observable.
Now that you grasp some basic concepts of reactive programming, I can explain you what RxKotlin is about.
According to the official definition,
RxKotlin is a lightweight library that adds convenient extension functions to RxJava. You can use RxJava with Kotlin out-of-the-box, but Kotlin has language features (such as extension functions) that can streamline usage of RxJava even more. RxKotlin aims to conservatively collect these conveniences in one centralized library, and standardize conventions for using RxJava with Kotlin.
So from what is said, we see that RxKotlin is a wrapper around RxJava which is itself an open-source implementation of the ReactiveX library that helps you create applications in the reactive programming style.
Now we can move on to the implementation of this concept into our shopping cart app.
Adding reactivity to the counter with RxKotlin
Now, head over to your build.gradle
file, then add the RxJava and RxKotlin libraries to your dependencies block:
//..app/build.gradle
implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0'
You remembered in the previous part we’ve added some listeners to our buttons to perform some operations over our cart. We’ll update this section, we’ll add a taste of reactivity using the really cool possibilities RxKotlin offers us.
Move to your ProductAdapter
file where you defined listeners,
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()
}
and amend this section like the following:
../app/src/main/java/yourPackage/ProductAdapter.kt
Observable.create(ObservableOnSubscribe<MutableList<CartItem>> {
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()
it.onNext(ShoppingCart.getCart())
}
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()
it.onNext(ShoppingCart.getCart())
}
}).subscribe { cart ->
var quantity = 0
cart.forEach { cartItem ->
quantity += cartItem.quantity
}
(itemView.context as MainActivity).cart_size.text = quantity.toString()
Toast.makeText(itemView.context, "Cart size $quantity", Toast.LENGTH_SHORT).show()
}
What we’ve done above is pretty simple. We’ve created an Observable from our shopping cart data as we know it is basically a list of CartItem
:
Observable.create(ObservableOnSubscribe<MutableList<CartItem>> {
Then whenever an item is added or removed, we pulled the new state of the shopping cart to the data stream with this line: it.onNext(ShoppingCart.getCart())
.
Next we subscribe to this stream of data, once we get it, we made some basic operation to get the updated shopping cart size and finally we display it on the counter: (itemView.context as MainActivity).cart_size.text = quantity.toString()
. Pretty simple, right?
You should agree that in just a few lines of code, we’ve added some reactivity to our shopping cart 😎.
Now if you run and test your app, you should see it behaving like expected. No more disappointment like in the previous article 😁.
Let’s finish our app by adding a review functionality with a Checkout button.
Adding the review functionality to the shopping cart
In this section, we’ll build an activity responsible for reviewing our shopping cart items as well as convenient details.
Create a cart_list_item
file and paste the following inside. This layout is responsible for handling the view of a single cart item. It simply contains the cart item image, its name, price and quantity.
<?xml version="1.0" encoding="utf-8"?>
//../app/src/main/java/res/layout/cart_list_item.xml
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardUseCompatPadding="true"
app:cardElevation="1.5dp"
android:layout_margin="2dp"
app:cardBackgroundColor="@android:color/white"
app:cardCornerRadius="2dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:gravity="center_vertical"
android:weightSum="1.5"
android:layout_height="match_parent">
<ImageView
android:layout_weight=".5"
android:scaleType="fitXY"
android:id="@+id/product_image"
android:layout_width="170dp"
android:layout_height="135dp"/>
<LinearLayout
android:layout_weight=".5"
android:gravity="center_vertical"
android:layout_marginRight="8dp"
android:padding="8dp"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="8dp">
<TextView
android:textSize="19sp"
android:textColor="@android:color/black"
android:id="@+id/product_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginTop="10dp"
android:textSize="16sp"
android:textStyle="bold"
android:id="@+id/product_price"
android:textColor="@android:color/holo_red_light"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:textStyle="bold"
android:layout_marginEnd="12dp"
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
android:padding="8dp"
android:textColor="@android:color/black"
android:textSize="16sp"
android:background="@drawable/round_background"
android:id="@+id/product_quantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="12dp"/>
</LinearLayout>
</android.support.v7.widget.CardView>
We’ll also a need adapter to render our shopping cart items properly.
Create a ShoppingCartAdapter.kt
file and paste the following inside:
//..app/src/main/java/yourPackage/ShoppingCartAdapter.kt
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.cart_list_item.view.*
class ShoppingCartAdapter(var context: Context, var cartItems: List<CartItem>) :
RecyclerView.Adapter<ShoppingCartAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, p1: Int): ShoppingCartAdapter.ViewHolder {
// The layout design used for each list item
val layout = LayoutInflater.from(context).inflate(R.layout.cart_list_item, parent, false)
return ViewHolder(layout)
}
// This returns the size of the list.
override fun getItemCount(): Int = cartItems.size
override fun onBindViewHolder(viewHolder: ShoppingCartAdapter.ViewHolder, position: Int) {
//we simply call the `bindItem` function here
viewHolder.bindItem(cartItems[position])
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindItem(cartItem: CartItem) {
// This displays the cart item information for each item
Picasso.get().load(cartItem.product.photos[0].filename).fit().into(itemView.product_image)
itemView.product_name.text = cartItem.product.name
itemView.product_price.text = "$${cartItem.product.price}"
itemView.product_quantity.text = cartItem.quantity.toString()
}
}
}
Now, we are a few to finish our mobile app. The next step is build the activity responsible for handing the review of our shopping cart. It will have the following rendering 😋
Create an empty activity named ShoppingCartActivity
or whatever you want. Then, move on to your activity_shopping_cart.xml
file and replace the content with the following:
//../app/src/main/java/res/layout/activity_shopping_cart.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#fffffdff"
android:layout_height="match_parent"
tools:context=".ShoppingCartActivity">
<android.support.design.widget.AppBarLayout
android:background="@android:color/transparent"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
app:titleTextColor="@color/colorAccent"
app:title="Shopping Cart"
android:background="@android:color/white"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/shopping_cart_recyclerView"
android:layout_margin="2dp"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<RelativeLayout
android:padding="8dp"
app:elevation="4dp"
android:layout_gravity="bottom"
android:elevation="15dp"
android:background="@color/colorAccent"
android:layout_width="match_parent"
android:layout_height="66dp"
tools:targetApi="lollipop">
<LinearLayout
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_marginLeft="15dp"
android:layout_height="wrap_content"
tools:ignore="RtlCompat">
<TextView
android:id="@+id/totalLabel"
android:textSize="18sp"
android:textStyle="bold"
android:text="Total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:layout_marginStart="18dp"
android:id="@+id/total_price"
android:textSize="24sp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="RtlCompat"/>
</LinearLayout>
<Button
android:textSize="19sp"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:padding="10dp"
android:layout_alignParentEnd="true"
android:text="Checkout"
android:textAllCaps="false"
android:background="@drawable/round_background"
android:textColor="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"/>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
The above layout contains a recycler view to display our shopping cart items, and a bottom bar containing the total price of our shopping and a Checkout button.
Also, replace the content of your ShoppingCartActivity
like the following:
//..app/src/main/java/yourPackage/ShoppingCartActivity.kt
import android.graphics.PorterDuff
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v4.content.ContextCompat
import android.support.v7.widget.LinearLayoutCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import kotlinx.android.synthetic.main.activity_shopping_cart.*
class ShoppingCartActivity : AppCompatActivity() {
lateinit var adapter: ShoppingCartAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_shopping_cart)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val upArrow = ContextCompat.getDrawable(this, R.drawable.abc_ic_ab_back_material)
upArrow?.setColorFilter(ContextCompat.getColor(this, R.color.colorPrimary), PorterDuff.Mode.SRC_ATOP)
supportActionBar?.setHomeAsUpIndicator(upArrow)
adapter = ShoppingCartAdapter(this, ShoppingCart.getCart())
adapter.notifyDataSetChanged()
shopping_cart_recyclerView.adapter = adapter
shopping_cart_recyclerView.layoutManager = LinearLayoutManager(this)
var totalPrice = ShoppingCart.getCart()
.fold(0.toDouble()) { acc, cartItem -> acc + cartItem.quantity.times(cartItem.product.price!!.toDouble()) }
total_price.text = "$${totalPrice}"
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> {
onBackPressed()
}
}
return super.onOptionsItemSelected(item)
}
}
In this activity, we are doing three essential things, first we provide our adapter with the shopping cart items to handle, then we configure and tell our recycler view that its adapter is ready to use and we assigned it, then we compute the total price from the shopping cart items and display it.
We get a reference to the arrow back icon, and tint its color to fit our design needs.
val upArrow = ContextCompat.getDrawable(this, R.drawable.abc_ic_ab_back_material)
upArrow?.setColorFilter(ContextCompat.getColor(this, R.color.colorPrimary), PorterDuff.Mode.SRC_ATOP)
supportActionBar?.setHomeAsUpIndicator(upArrow)
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> {
onBackPressed()
}
}
return super.onOptionsItemSelected(item)
}
This block of code simply redirects the user to the previous screen/activity when the back arrow icon is clicked.
Now we need a way to link our two screens, the MainActivity
one and the ShoppingCartActivity
one. But how? 🤔 We just need to add a click listener to the showCart
button defined in the MainActivity
class, the listener handler will head us to the ShoppingCartActivity
screen.
So add this block of code into your MainActivity.kt
file after the getProducts
method:
//..app/src/main/java/yourPackage/MainActivity.kt
showCart.setOnClickListener {
startActivity(Intent(this, ShoppingCartActivity::class.java))
}
Our app is complete now with all the expected features. You can run and test it, hopefully everything should be working fine. Otherwise, check if you haven’t missed any step.
Conclusion
Finally we reached the end of this tutorial series. I think this part has been more useful to you than others because you have learned a new concept : Reactive programming and put it into practice.
You can grab the source code for the last part here; if you want and you can even fork the repo to add more features to the app 😉.
12 March 2019
by Ethiel Adiassa