Home
Blog
Community

How to Build an Android Ecommerce App with Medusa

Mar 02, 2023 by

Arjuna Sky Kok

Arjuna Sky Kok

This article explains how to build an Android ecommerce app with Medusa.
In addition to ecommerce websites, many ecommerce startups now require mobile applications to enhance the shopping experience for users with features such as push notifications, personalization, less friction, and more.
Medusa is a set of ecommerce building blocks that gives developers full control over building an ecommerce application. Its modules make up a headless backend that any frontend, storefront, app, or admin dashboard, can connect to through REST APIs. “Headless” in this case means that the backend and frontend are separate.
This article explains how to build an Android ecommerce app with Medusa. The code source of the Android client is available on GitHub.
Here is a preview of what your application should look like at the end of this tutorial.

Prerequisites

To follow this guide, it's essential to have the following:

Set Up Medusa Server

Install the Medusa CLI app with the following command:
yarn global add @medusajs/medusa-cli
Create a new Medusa project:
medusa new my-medusa-server --seed
By default, it will use SQLite database. You can configure it to use PostgreSQL database. Refer to the documentation for the details.
Start the server with the commands below:
cd my-medusa-server
medusa develop
Your server runs at port 9000. To verify that the server is working properly, you can open the URL
Copy to clipboard
http://localhost:9000/store/products
with your browser. You should get the JSON data of products available on your server.

Set Up the Android Ecommerce Project

Begin by launching Android Studio and create a new project. In the New Project window, choose Empty Compose Activity.
In the following screen, choose API 33: Android Tiramisu in the Minimum SDK field.
You can name your application and your package as you wish. In this tutorial, the application's name is Medusa Android Application and the package name is
Copy to clipboard
com.medusajs.android.medusaandroidapplication
.

Install Dependencies

Once the project is ready, edit the application
Copy to clipboard
build.gradle
and add these libraries in the
Copy to clipboard
dependencies
block:
dependencies {
...
// Add the new dependencies here
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.github.skydoves:landscapist-glide:2.1.0'
implementation 'androidx.navigation:navigation-compose:2.5.3'
}
Sync the Gradle build file by clicking the
Copy to clipboard
Sync Now
button. The Retrofit library allows you to connect to API with objects, while the Glide library facilitates the loading of remote images. The navigation connects screens in your Android application.
Create an XML file named
Copy to clipboard
network_security_config.xml
inside
Copy to clipboard
app/res/xml
and add the following content:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:android="http://schemas.android.com/apk/res/android">
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>
Add this network rule to connect to your
Copy to clipboard
localhost
in the Android emulator without https. The Android emulator recognizes
Copy to clipboard
localhost
as 10.0.2.2.
Edit
Copy to clipboard
AndroidManifest.xml
in
Copy to clipboard
app/manifests
. Add these lines inside the
Copy to clipboard
manifest
node:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Add them here -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />
<application> ... </application>
</manifest>
Add this line as an attribute in the
Copy to clipboard
application
node:
android:networkSecurityConfig="@xml/network_security_config"

Connect the Android Project to the Medusa Server

You have to model the object that represents the API in the Medusa Server. Create the package
Copy to clipboard
model
in
Copy to clipboard
app/java/com/medusajs/android/medusaandroidapplication
. Inside the package, create the
Copy to clipboard
Response
class, and replace the content with the following code:
package com.medusajs.android.medusaandroidapplication.model
data class ProductsResult(val products: List<Product>)
data class ProductResult(val product: Product)
data class CartResult(val cart: Cart)
data class CartRequest(val items: List<Item>)
data class Product(
val id: String?,
val title: String?,
val thumbnail: String?,
val variants: List<Variant>
)
data class Variant(
val id: String?,
val prices: List<Price>
)
class Price {
var currency_code: String = ""
var amount: Int = 0
fun getPrice() : String {
return "$currency_code $amount"
}
}
data class Item(
val variant_id: String,
val title: String?,
val thumbnail: String?,
val quantity: Int
)
data class Cart(
val id: String?,
val items: List<Item>
)
data class LineItem(
val variant_id: String,
val quantity: Int
)
Each data class represents the JSON data structure you get from or send to the Medusa server. You can take a look at the complete API reference of the Medusa server in this store API reference.
Check if the result of an API call has this structure:
{
products: {
{
id: ...,
title: ...
},
{
id: ...,
title: ...
}
}
}
If that is the case, then the correspondence data class are these classes:
data class ProductsResult(val products: List<Product>)
data class Product(
val id: String?,
val title: String?
)
You need to access the API endpoints. Create the
Copy to clipboard
MedusaService
interface in
Copy to clipboard
app/java/com/medusajs/android/medusaandroidapplication/``model
and replace the content with the following code:
package com.medusajs.android.medusaandroidapplication.model
import retrofit2.Call
import retrofit2.http.*
interface MedusaService {
@GET("/store/products")
fun retrieveProducts(): Call<ProductsResult>
@GET("/store/products/{id}")
fun getProduct(@Path("id") id: String) : Call<ProductResult>
@Headers("Content-Type: application/json")
@POST("/store/carts")
fun createCart(@Body cart: CartRequest): Call<CartResult>
@Headers("Content-Type: application/json")
@POST("/store/carts/{id}/line-items")
fun addProductToCart(@Path("id") id: String, @Body lineItem: LineItem): Call<CartResult>
@GET("/store/carts/{id}")
fun getCart(@Path("id") id: String) : Call<CartResult>
}
As you can see, there are two annotations. One is for the GET request and the other is for the POST request. The argument for the annotation is the API endpoint in the Medusa server. The argument
Copy to clipboard
/store/products
mean
Copy to clipboard
http://``localhost:9000``/store/products
. The return result represents the result type you will get.
The
Copy to clipboard
@Body
represents the JSON data you send to the API endpoint. The
Copy to clipboard
@Path
represents the parameter of the API endpoint.
This is only an interface. You need to implement it. Create the
Copy to clipboard
ProductsRetriever
class inside the
Copy to clipboard
app/java/com/medusajs/android/medusaandroidapplication/model
package and replace the content with the following code:
package com.medusajs.android.medusaandroidapplication.model
import retrofit2.Callback
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class ProductsRetriever {
private val service: MedusaService
companion object {
const val BASE_URL = "http://10.0.2.2:9000"
}
init {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
service = retrofit.create(MedusaService::class.java)
}
fun getProducts(callback: Callback<ProductsResult>) {
val call = service.retrieveProducts()
call.enqueue(callback)
}
fun getProduct(productId: String, callback: Callback<ProductResult>) {
val call = service.getProduct(productId)
call.enqueue(callback)
}
fun createCart(cartId: String, variantId: String, callback: Callback<CartResult>) {
if (cartId.isNotEmpty()) {
val lineItemRequest = LineItem(variantId, 1)
val call = service.addProductToCart(cartId, lineItemRequest)
call.enqueue(callback)
} else {
val items = listOf(
Item(variant_id=variantId, quantity=1, thumbnail=null, title=null)
)
val cartRequest = CartRequest(items)
val call = service.createCart(cartRequest)
call.enqueue(callback)
}
}
fun getCart(cartId: String, callback: Callback<CartResult>) {
if (cartId.isNotEmpty()) {
val call = service.getCart(cartId)
call.enqueue(callback)
}
}
}
In this class, you initialize the
Copy to clipboard
retrofit
variable with the base URL
Copy to clipboard
http://10.0.2.2:9000
and the Gson converter to handle JSON. Remember that the emulator knows the
Copy to clipboard
localhost
as
Copy to clipboard
10.0.2.2
. Then you create a service with the
Copy to clipboard
create
method from the
Copy to clipboard
retrofit
variable with
Copy to clipboard
MedusaService
.
You use this
Copy to clipboard
service
variable within the implemented methods. You execute the method defined in the interface, resulting in the call object. Then you call the
Copy to clipboard
enqueue
method from this object with the callback argument. After the API endpoint is called, this callback will be executed.

Products List Screen

Now that you can connect to API endpoints, you can create a UI interface for the products list. Create a new file called
Copy to clipboard
ProductsList.kt
in
Copy to clipboard
app/java/com/medusajs/android/``medusaandroidapplication``ui
and replace the content with the following code:
package com.medusajs.android.medusaandroidapplication.ui
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.medusajs.android.medusaandroidapplication.model.Product
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.glide.GlideImage
@Composable
fun ProductsList(products: List<Product>,
onItemSelected: (String) -> Unit,
onCartButtonClick: () -> Unit) {
Column(modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
onCartButtonClick()
}) {
Text("My Cart")
}
products.forEach { product ->
Column(
modifier = Modifier
.padding(8.dp)
.border(BorderStroke(2.dp, Color.Gray))
.clickable {
onItemSelected(product.id!!)
}
,
horizontalAlignment = Alignment.CenterHorizontally
) {
GlideImage(
imageModel = { product.thumbnail!! },
imageOptions = ImageOptions(
contentScale = ContentScale.Crop,
requestSize = IntSize(400,600),
alignment = Alignment.Center
)
)
Text(product.title!!, fontWeight = FontWeight.Bold)
Text(product.variants[0].prices[0].getPrice())
}
}
}
}
In this
Copy to clipboard
ProductsList
function, you create a button to go to the cart screen and a list of products. Each card is composed of a remote image, a title of the product, and the price.
Each product in Medusa has more than one variant. Each variant can have many prices (depending on which region). For this mobile ecommerce tutorial, each product uses the first variant and the first price from the variant.
The purpose of this decision is to make the tutorial simple. But in production, you can make a different architecture, such as displaying products with their variants.

Product Info Screen

Now it’s time to create a detail page. On that page, a user can add the product to their shopping cart.
Create the
Copy to clipboard
ProductItem.kt
file inside
Copy to clipboard
app/java/com/medusajs/androidmedusaandroidapplication/ui
and replace the content with the following code:
package com.medusajs.android.<your application name>.ui
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.medusajs.android.medusaandroidapplication.model.CartResult
import com.medusajs.android.medusaandroidapplication.model.Product
import com.medusajs.android.medusaandroidapplication.model.ProductsRetriever
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.glide.GlideImage
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@Composable
fun ProductItem(product: Product,
cartId: String,
onCartChange: (String) -> Unit) {
Column(
modifier = Modifier
.padding(8.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
GlideImage(
imageModel = { product.thumbnail!! },
imageOptions = ImageOptions(
alignment = Alignment.Center,
requestSize = IntSize(800,1200)
)
)
Text(product.title!!, fontSize = 30.sp)
Text(product.variants[0].prices[0].getPrice())
Button(onClick = {
val productsRetriever = ProductsRetriever()
val callback = object : Callback<CartResult> {
override fun onFailure(call: Call<CartResult>, t: Throwable) {
Log.e("MainActivity", t.message!!)
}
override fun onResponse(call: Call<CartResult>, response: Response<CartResult>) {
response.isSuccessful.let {
response.body()?.let { c ->
onCartChange(c.cart.id!!)
}
}
}
}
productsRetriever.createCart(cartId, product.variants[0].id!!, callback)
}) {
Text("Add 1 Item to Cart")
}
}
}
In the button's callback, you create an object of
Copy to clipboard
Callback<CartResult>
with two methods:
Copy to clipboard
onFailure
and
Copy to clipboard
onResponse
. If the API call is successful, the
Copy to clipboard
onReponse
method will be triggered. You call
Copy to clipboard
onCartChange
with the cart id you receive from the server. The method saves the cart id to a variable so you can reuse the cart id later. If the API call is unsuccessful, the
Copy to clipboard
onFailure
method will be called. In this case, you just log the error.
You call the
Copy to clipboard
createCart
method with the variant id of the product. To simplify the tutorial, you just send the first variant of the product. Then you can only add one piece of the product.
If you want to add two product units, you must press the button twice. You also send the cart ID. Initially, the cart ID will be 'null’, in which case the server will create a cart for you. However, if it is not null, you will reuse the existing cart, allowing you to add the product.
To create a cart, you call the
Copy to clipboard
createCart
method with the
Copy to clipboard
CartRequest
object. To add an item to the cart, call the
Copy to clipboard
addProductToCart
method with the
Copy to clipboard
LineItem
object.
You can verify the JSON data the Medusa server requires in the API documentation.
For example, to add a line item, the expected payload is:
{
"variant_id": "string",
"quantity": 0,
"metadata": {}
}
Notice the JSON fields (
Copy to clipboard
variant_id
and
Copy to clipboard
quantity
) are the same as the fields in the
Copy to clipboard
LineItem
data class.

Create a Cart

You need to create the
Copy to clipboard
ViewModel
. Create
Copy to clipboard
CartViewModel
in
Copy to clipboard
app/java/com/medusajs/android/``m``edusaandroidapplication/model
:
package com.medusajs.android.medusaandroidapplication.model
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
data class CartModel (
val title: String,
val quantity: Int,
val thumbnail: String,
)
class CartViewModel : ViewModel() {
private val _cartState = MutableStateFlow(emptyList<CartModel>())
val cartState: StateFlow<List<CartModel>> = _cartState.asStateFlow()
fun setCart(cartModels: List<CartModel>) {
_cartState.value = cartModels
}
}
The
Copy to clipboard
ViewModel
has
Copy to clipboard
_cartState
that is a
Copy to clipboard
MutableStateFlow
holding
Copy to clipboard
CartModel
.
Users might want to see what products they have added to their shopping cart. You need to create a screen for the cart. Create
Copy to clipboard
CartCompose
in
Copy to clipboard
app/java/com/medusajs/android/<your application name>/ui
and replace its content with the following code:
package com.raywenderlich.android.medusaandroidapplication.ui
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.medusajs.android.medusaandroidapplication.model.CartModel
import com.medusajs.android.medusaandroidapplication.model.CartResult
import com.medusajs.android.medusaandroidapplication.model.CartViewModel
import com.medusajs.android.medusaandroidapplication.model.ProductsRetriever
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.glide.GlideImage
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@Composable
fun CartCompose(cartId: String,
cartViewModel: CartViewModel = viewModel()
) {
val cartStates by cartViewModel.cartState.collectAsState()
val callback = object : Callback<CartResult> {
override fun onFailure(call: Call<CartResult>, t: Throwable) {
Log.e("MainActivity", t.message!!)
}
override fun onResponse(call: Call<CartResult>, response: Response<CartResult>) {
response.isSuccessful.let {
response.body()?.let { c ->
val cartModels : MutableList<CartModel> = mutableListOf()
c.cart.items.forEach { item ->
val title = item.title!!
val thumbnail = item.thumbnail!!
val quantity = item.quantity
val cartModel = CartModel(title, quantity, thumbnail)
cartModels.add(cartModel)
}
cartViewModel.setCart(cartModels.toList())
}
}
}
}
val productsRetriever = ProductsRetriever()
productsRetriever.getCart(cartId, callback)
Column(modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("My Cart")
cartStates.forEach { cartState ->
Row(modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween) {
GlideImage(
imageModel = { cartState.thumbnail },
imageOptions = ImageOptions(
alignment = Alignment.Center,
requestSize = IntSize(200,300)
)
)
Text(cartState.title)
Text("${cartState.quantity} pcs")
}
}
}
}
You use
Copy to clipboard
cartViewModel
and a
Copy to clipboard
ViewModel
to hold the products you added to the cart.
In the
Copy to clipboard
onResponse
method, after a successful API call, you set the
Copy to clipboard
ViewModel
with the cart JSON object from the server.
You call the
Copy to clipboard
getCart
method with the cart id argument to get the information about the cart.

Set up the Navigation

All that’s left is to create the navigation that connects the screens. Create
Copy to clipboard
MainAppCompose
inside
Copy to clipboard
app/java/com/medusajs/ui
and replace the content with the following code:
package com.medusajs.android.medusaandroidapplication.ui
import android.util.Log
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.medusajs.android.medusaandroidapplication.model.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@Composable
fun MainApp(products: List<Product>) {
var cartId by rememberSaveable { mutableStateOf("") }
val navController = rememberNavController()
var product : Product? = null
NavHost(navController = navController, startDestination = "list") {
composable("list") {
ProductsList(products,
onItemSelected = { product_id ->
navController.navigate("item/$product_id")
},
onCartButtonClick = {
navController.navigate("cart")
}
)
}
composable(
"item/{product_id}",
arguments = listOf(navArgument("product_id") { type = NavType.StringType})) { it ->
val productId = it.arguments?.getString("product_id")!!
val callback = object : Callback<ProductResult> {
override fun onFailure(call: Call<ProductResult>, t: Throwable) {
Log.e("MainActivity", t.message!!)
}
override fun onResponse(call: Call<ProductResult>, response: Response<ProductResult>) {
response.isSuccessful.let {
response.body()?.let { p ->
product = p.product
}
}
}
}
val productsRetriever = ProductsRetriever()
productsRetriever.getProduct(productId, callback)
product?.let { ProductItem(it, cartId, onCartChange = { cartId = it }) }
}
composable("cart") {
CartCompose(cartId)
}
}
}
You created
Copy to clipboard
NavHost
, which is composed of three compose functions or screens. You navigate from one screen to another screen with the
Copy to clipboard
navigate
method of the
Copy to clipboard
navController
object.
Finally, edit
Copy to clipboard
MainActivity
that is inside the
Copy to clipboard
app/java/com/medusajs/android/medusaandroidapplication
package to call the
Copy to clipboard
MainApp
that connects the screens. Replace the content with the following code:
package com.medusajs.android.medusaandroidapplication
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.*
import com.medusajs.android.medusaandroidapplication.model.Product
import com.medusajs.android.medusaandroidapplication.model.ProductsResult
import com.medusajs.android.medusaandroidapplication.model.ProductsRetriever
import com.medusajs.android.medusaandroidapplication.ui.MainApp
import com.medusajs.android.medusaandroidapplication.ui.theme.MedusaAndroidApplicationTheme
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MainActivity : ComponentActivity() {
private val productsRetriever = ProductsRetriever()
private var products : List<Product> = emptyList()
private val callback = object : Callback<ProductsResult> {
override fun onFailure(call: Call<ProductsResult>, t: Throwable) {
Log.e("MainActivity", t.message!!)
}
override fun onResponse(call: Call<ProductsResult>, response: Response<ProductsResult>) {
response.isSuccessful.let {
products = response.body()?.products ?: emptyList()
setContentWithProducts()
}
}
}
fun setContentWithProducts() {
setContent {
MedusaAndroidApplicationTheme {
Scaffold(
topBar = { TopAppBar(title = { Text(text = "Medusa App") }) }
) {
MainApp(products = products)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
productsRetriever.getProducts(callback)
}
}

Test the Application

Now that you have written the code, it's time to build the application. Make sure the Medusa server is running and run the following command:
cd my-medusa-server
medusa develop
Build and run the application in the Android emulator. But first, you need to create a device in Device Manager. You will get the screen of the products list.
Click one of the products, then you'll see the product detail page.
Add some products to your shopping cart in the products list screen; click the button to view the cart.

Conclusion

This tutorial provides an overview of how to build an Android ecommerce application with Medusa. This application provides basic functionalities, but you can implement more functionalities to enhance it:
Visit the Medusa store API reference to discover all the possibilities to build your ecommerce application. Or check out our Next.js-based commerce template for your next project.
Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord.

Share this post

Try Medusa

Spin up your environment in a few minutes.

Get started