Android | Kotlin | API | MVVM | Retrofit | Dagger | Hilt | Coroutines | Jetpack Compose

Tutorial for using modern Android development architecture components and best practices to make API calls

Arunabh Das
Developers Inc
12 min readMay 28, 2023

--

Android | Kotlin | MVVM | Retrofit | Dagger | Hilt | Coroutine | Room | Jetpack Compose

Prerequisites

Root build.gradle

In order to ensure that our Android project has all the dependencies, let’s first check our root level build.gradle to ensure that it contains the below dependencies

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
androidx_app_compat_version = '1.3.1'
compose_version = '1.4.6'
kotlin_version = '1.8.20'
}
repositories {
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0-alpha01'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}


plugins {
id 'com.android.application' version '8.2.0-alpha01' apply false
id 'com.android.library' version '8.2.0-alpha01' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
id 'com.google.dagger.hilt.android' version '2.44' apply false
}

task clean(type: Delete) {
delete rootProject.buildDir
}

Project build.gradle

Let’s ensure that the project level build.gradle has the below dependencies

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}

android {
namespace 'ai.cloudcnctrai.cloudcnctrai'
compileSdk 33

defaultConfig {
applicationId "ai.cloudcnctrai.cloudcnctrai"
minSdk 23
targetSdk 32
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
viewBinding true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion '1.8.20'
}
namespace 'ai.cloudcnctrai.cloudcnctrai'
}

dependencies {

// Import the Compose BOM
implementation platform('androidx.compose:compose-bom:2023.04.01')

// Core
implementation 'androidx.core:core-ktx'

// Compose dependencies
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.ui:ui-tooling-preview"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx'
implementation "androidx.compose.material:material"


implementation 'androidx.activity:activity-compose'

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.5.0-alpha04'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'

// Compose dependencies
implementation "androidx.lifecycle:lifecycle-viewmodel-compose"
implementation "androidx.navigation:navigation-compose"
implementation "com.google.accompanist:accompanist-flowlayout:0.17.0"

// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'

// Coroutine Lifecycle Scopes
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"

//Dagger - Hilt
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
kapt "androidx.hilt:hilt-compiler:1.0.0-alpha01"
implementation 'androidx.hilt:hilt-navigation-compose:1.1.0-alpha01'
implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0'

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
implementation "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2"

// Retrofit2
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'

// Coil
implementation("io.coil-kt:coil-compose:1.4.0")

// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

For the purpose of this tutorial, we are going to parse the following API and display the products

https://fakestoreapi.com/products

The JSON response from the above API looks as follows

[
{
"id": 1,
"title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
"price": 109.95,
"description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
"rating": {
"rate": 3.9,
"count": 120
}
},
{
"id": 2,
"title": "Mens Casual Premium Slim Fit T-Shirts ",
"price": 22.3,
"description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg",
"rating": {
"rate": 4.1,
"count": 259
}
},
{
"id": 3,
"title": "Mens Cotton Jacket",
"price": 55.99,
"description": "great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling, traveling or other outdoors. Good gift choice for you or your family member. A warm hearted love to Father, husband or son in this thanksgiving or Christmas Day.",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg",
"rating": {
"rate": 4.7,
"count": 500
}
},
{
"id": 4,
"title": "Mens Casual Slim Fit",
"price": 15.99,
"description": "The color could be slightly different between on the screen and in practice. / Please note that body builds vary by person, therefore, detailed size information should be reviewed below on the product description.",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/71YXzeOuslL._AC_UY879_.jpg",
"rating": {
"rate": 2.1,
"count": 430
}
},
{
"id": 5,
"title": "John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet",
"price": 695,
"description": "From our Legends Collection, the Naga was inspired by the mythical water dragon that protects the ocean's pearl. Wear facing inward to be bestowed with love and abundance, or outward for protection.",
"category": "jewelery",
"image": "https://fakestoreapi.com/img/71pWzhdJNwL._AC_UL640_QL65_ML3_.jpg",
"rating": {
"rate": 4.6,
"count": 400
}
},
{
"id": 6,
"title": "Solid Gold Petite Micropave ",
"price": 168,
"description": "Satisfaction Guaranteed. Return or exchange any order within 30 days.Designed and sold by Hafeez Center in the United States. Satisfaction Guaranteed. Return or exchange any order within 30 days.",
"category": "jewelery",
"image": "https://fakestoreapi.com/img/61sbMiUnoGL._AC_UL640_QL65_ML3_.jpg",
"rating": {
"rate": 3.9,
"count": 70
}
},
{
"id": 7,
"title": "White Gold Plated Princess",
"price": 9.99,
"description": "Classic Created Wedding Engagement Solitaire Diamond Promise Ring for Her. Gifts to spoil your love more for Engagement, Wedding, Anniversary, Valentine's Day...",
"category": "jewelery",
"image": "https://fakestoreapi.com/img/71YAIFU48IL._AC_UL640_QL65_ML3_.jpg",
"rating": {
"rate": 3,
"count": 400
}
},
{
"id": 8,
"title": "Pierced Owl Rose Gold Plated Stainless Steel Double",
"price": 10.99,
"description": "Rose Gold Plated Double Flared Tunnel Plug Earrings. Made of 316L Stainless Steel",
"category": "jewelery",
"image": "https://fakestoreapi.com/img/51UDEzMJVpL._AC_UL640_QL65_ML3_.jpg",
"rating": {
"rate": 1.9,
"count": 100
}
},
{
"id": 9,
"title": "WD 2TB Elements Portable External Hard Drive - USB 3.0 ",
"price": 64,
"description": "USB 3.0 and USB 2.0 Compatibility Fast data transfers Improve PC Performance High Capacity; Compatibility Formatted NTFS for Windows 10, Windows 8.1, Windows 7; Reformatting may be required for other operating systems; Compatibility may vary depending on user’s hardware configuration and operating system",
"category": "electronics",
"image": "https://fakestoreapi.com/img/61IBBVJvSDL._AC_SY879_.jpg",
"rating": {
"rate": 3.3,
"count": 203
}
},
{
"id": 10,
"title": "SanDisk SSD PLUS 1TB Internal SSD - SATA III 6 Gb/s",
"price": 109,
"description": "Easy upgrade for faster boot up, shutdown, application load and response (As compared to 5400 RPM SATA 2.5” hard drive; Based on published specifications and internal benchmarking tests using PCMark vantage scores) Boosts burst write performance, making it ideal for typical PC workloads The perfect balance of performance and reliability Read/write speeds of up to 535MB/s/450MB/s (Based on internal testing; Performance may vary depending upon drive capacity, host device, OS and application.)",
"category": "electronics",
"image": "https://fakestoreapi.com/img/61U7T1koQqL._AC_SX679_.jpg",
"rating": {
"rate": 2.9,
"count": 470
}
},
{
"id": 11,
"title": "Silicon Power 256GB SSD 3D NAND A55 SLC Cache Performance Boost SATA III 2.5",
"price": 109,
"description": "3D NAND flash are applied to deliver high transfer speeds Remarkable transfer speeds that enable faster bootup and improved overall system performance. The advanced SLC Cache Technology allows performance boost and longer lifespan 7mm slim design suitable for Ultrabooks and Ultra-slim notebooks. Supports TRIM command, Garbage Collection technology, RAID, and ECC (Error Checking & Correction) to provide the optimized performance and enhanced reliability.",
"category": "electronics",
"image": "https://fakestoreapi.com/img/71kWymZ+c+L._AC_SX679_.jpg",
"rating": {
"rate": 4.8,
"count": 319
}
},
{
"id": 12,
"title": "WD 4TB Gaming Drive Works with Playstation 4 Portable External Hard Drive",
"price": 114,
"description": "Expand your PS4 gaming experience, Play anywhere Fast and easy, setup Sleek design with high capacity, 3-year manufacturer's limited warranty",
"category": "electronics",
"image": "https://fakestoreapi.com/img/61mtL65D4cL._AC_SX679_.jpg",
"rating": {
"rate": 4.8,
"count": 400
}
},
{
"id": 13,
"title": "Acer SB220Q bi 21.5 inches Full HD (1920 x 1080) IPS Ultra-Thin",
"price": 599,
"description": "21. 5 inches Full HD (1920 x 1080) widescreen IPS display And Radeon free Sync technology. No compatibility for VESA Mount Refresh Rate: 75Hz - Using HDMI port Zero-frame design | ultra-thin | 4ms response time | IPS panel Aspect ratio - 16: 9. Color Supported - 16. 7 million colors. Brightness - 250 nit Tilt angle -5 degree to 15 degree. Horizontal viewing angle-178 degree. Vertical viewing angle-178 degree 75 hertz",
"category": "electronics",
"image": "https://fakestoreapi.com/img/81QpkIctqPL._AC_SX679_.jpg",
"rating": {
"rate": 2.9,
"count": 250
}
},
{
"id": 14,
"title": "Samsung 49-Inch CHG90 144Hz Curved Gaming Monitor (LC49HG90DMNXZA) – Super Ultrawide Screen QLED ",
"price": 999.99,
"description": "49 INCH SUPER ULTRAWIDE 32:9 CURVED GAMING MONITOR with dual 27 inch screen side by side QUANTUM DOT (QLED) TECHNOLOGY, HDR support and factory calibration provides stunningly realistic and accurate color and contrast 144HZ HIGH REFRESH RATE and 1ms ultra fast response time work to eliminate motion blur, ghosting, and reduce input lag",
"category": "electronics",
"image": "https://fakestoreapi.com/img/81Zt42ioCgL._AC_SX679_.jpg",
"rating": {
"rate": 2.2,
"count": 140
}
},
{
"id": 15,
"title": "BIYLACLESEN Women's 3-in-1 Snowboard Jacket Winter Coats",
"price": 56.99,
"description": "Note:The Jackets is US standard size, Please choose size as your usual wear Material: 100% Polyester; Detachable Liner Fabric: Warm Fleece. Detachable Functional Liner: Skin Friendly, Lightweigt and Warm.Stand Collar Liner jacket, keep you warm in cold weather. Zippered Pockets: 2 Zippered Hand Pockets, 2 Zippered Pockets on Chest (enough to keep cards or keys)and 1 Hidden Pocket Inside.Zippered Hand Pockets and Hidden Pocket keep your things secure. Humanized Design: Adjustable and Detachable Hood and Adjustable cuff to prevent the wind and water,for a comfortable fit. 3 in 1 Detachable Design provide more convenience, you can separate the coat and inner as needed, or wear it together. It is suitable for different season and help you adapt to different climates",
"category": "women's clothing",
"image": "https://fakestoreapi.com/img/51Y5NI-I5jL._AC_UX679_.jpg",
"rating": {
"rate": 2.6,
"count": 235
}
},
{
"id": 16,
"title": "Lock and Love Women's Removable Hooded Faux Leather Moto Biker Jacket",
"price": 29.95,
"description": "100% POLYURETHANE(shell) 100% POLYESTER(lining) 75% POLYESTER 25% COTTON (SWEATER), Faux leather material for style and comfort / 2 pockets of front, 2-For-One Hooded denim style faux leather jacket, Button detail on waist / Detail stitching at sides, HAND WASH ONLY / DO NOT BLEACH / LINE DRY / DO NOT IRON",
"category": "women's clothing",
"image": "https://fakestoreapi.com/img/81XH0e8fefL._AC_UY879_.jpg",
"rating": {
"rate": 2.9,
"count": 340
}
},
{
"id": 17,
"title": "Rain Jacket Women Windbreaker Striped Climbing Raincoats",
"price": 39.99,
"description": "Lightweight perfet for trip or casual wear---Long sleeve with hooded, adjustable drawstring waist design. Button and zipper front closure raincoat, fully stripes Lined and The Raincoat has 2 side pockets are a good size to hold all kinds of things, it covers the hips, and the hood is generous but doesn't overdo it.Attached Cotton Lined Hood with Adjustable Drawstrings give it a real styled look.",
"category": "women's clothing",
"image": "https://fakestoreapi.com/img/71HblAHs5xL._AC_UY879_-2.jpg",
"rating": {
"rate": 3.8,
"count": 679
}
},
{
"id": 18,
"title": "MBJ Women's Solid Short Sleeve Boat Neck V ",
"price": 9.85,
"description": "95% RAYON 5% SPANDEX, Made in USA or Imported, Do Not Bleach, Lightweight fabric with great stretch for comfort, Ribbed on sleeves and neckline / Double stitching on bottom hem",
"category": "women's clothing",
"image": "https://fakestoreapi.com/img/71z3kpMAYsL._AC_UY879_.jpg",
"rating": {
"rate": 4.7,
"count": 130
}
},
{
"id": 19,
"title": "Opna Women's Short Sleeve Moisture",
"price": 7.95,
"description": "100% Polyester, Machine wash, 100% cationic polyester interlock, Machine Wash & Pre Shrunk for a Great Fit, Lightweight, roomy and highly breathable with moisture wicking fabric which helps to keep moisture away, Soft Lightweight Fabric with comfortable V-neck collar and a slimmer fit, delivers a sleek, more feminine silhouette and Added Comfort",
"category": "women's clothing",
"image": "https://fakestoreapi.com/img/51eg55uWmdL._AC_UX679_.jpg",
"rating": {
"rate": 4.5,
"count": 146
}
},
{
"id": 20,
"title": "DANVOUY Womens T Shirt Casual Cotton Short",
"price": 12.99,
"description": "95%Cotton,5%Spandex, Features: Casual, Short Sleeve, Letter Print,V-Neck,Fashion Tees, The fabric is soft and has some stretch., Occasion: Casual/Office/Beach/School/Home/Street. Season: Spring,Summer,Autumn,Winter.",
"category": "women's clothing",
"image": "https://fakestoreapi.com/img/61pHAEJ4NML._AC_UX679_.jpg",
"rating": {
"rate": 3.6,
"count": 145
}
}
]

To follow along, one may checkout the feature/steps/step1 branch from the repo below

On running the project, one can see that the Products tab looks as follows

Initial state of Products screen

With the network/data/model selected, one may use the JSON to Kotlin data class converter to get the model class definitions as below

JSON to Kotlin Data Class

This gives us the following model classes

class Products : ArrayList<ProductModel>()


data class ProductModel(
val id: Int,
val title: String,
val description: String,
val category: String,
val image: String,
val price: Double,
val rating: Rating
)

data class Rating(
val count: Int,
val rate: Double
)

We may now add PRODUCT_ENDPOINT to Constants.kt as below

package ai.cloudcnctrai.cloudcnctrai.network.common

class Constants {
companion object {

const val BASE_URL_WEATHER = "http://api.openweathermap.org"
const val BASE_URL= "https://www.freetogame.com/api/"
const val GAMES_ENDPOINT = "games"
const val PRODUCTS_ENDPOINT = "https://fakestoreapi.com/products"
}
}

We may now add the ProductApi interface under the network/data package as below

import ai.cloudcnctrai.cloudcnctrai.network.common.Constants
import ai.cloudcnctrai.cloudcnctrai.network.data.model.GameModel
import ai.cloudcnctrai.cloudcnctrai.network.data.model.ProductModel
import retrofit2.Response
import retrofit2.http.GET

interface ProductApi {
@GET(Constants.PRODUCTS_ENDPOINT)
suspend fun getProducts(): Response<List<ProductModel>>
}

We may now create the ProductService class under the network/data package as below (injecting ProductApi)

import ai.cloudcnctrai.cloudcnctrai.network.data.model.ProductModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject

class ProductService @Inject constructor(private val productApi: ProductApi){
suspend fun getProducts(): List<ProductModel> {
return withContext(Dispatchers.IO) {
val products = productApi.getProducts()
products.body() ?: emptyList()
}
}
}

We may now create the ProductItem data class under network/domain/item package as below

import ai.cloudcnctrai.cloudcnctrai.network.data.model.ProductModel
import ai.cloudcnctrai.cloudcnctrai.network.data.model.Rating

data class ProductItem (
val id: Int,
val title: String,
val description: String,
val category: String,
val image: String,
val price: Double,
val rating: Rating
)

fun ProductModel.toProductItem() = ProductItem(id, title, description, category, image, price, rating)

We may now create the ProductRepository class under network/repo package as below (injecting ProductService)

import ai.cloudcnctrai.cloudcnctrai.network.data.GameService
import ai.cloudcnctrai.cloudcnctrai.network.data.ProductService
import ai.cloudcnctrai.cloudcnctrai.network.domain.item.GameItem
import ai.cloudcnctrai.cloudcnctrai.network.domain.item.ProductItem
import ai.cloudcnctrai.cloudcnctrai.network.domain.item.toGameItem
import ai.cloudcnctrai.cloudcnctrai.network.domain.item.toProductItem
import javax.inject.Inject

class ProductRepository @Inject constructor(private val productService: ProductService) {
suspend fun getProducts(): List<ProductItem> {
return productService.getProducts().map {
it.toProductItem()
}
}
}

We may now create the GetProductsUseCase class under network/domain package as below (injecting ProductRepository)

import ai.cloudcnctrai.cloudcnctrai.network.domain.item.GameItem
import ai.cloudcnctrai.cloudcnctrai.network.domain.item.ProductItem
import ai.cloudcnctrai.cloudcnctrai.network.repo.GameRepository
import ai.cloudcnctrai.cloudcnctrai.network.repo.ProductRepository
import javax.inject.Inject


class GetProductsUseCase @Inject constructor(
private val productRepository: ProductRepository
){
suspend operator fun invoke(): List<ProductItem> {
return productRepository.getProducts()

}
}

We may now create the MainViewModel class under viewmodel package (injecting GetProductsUseCase) as below

@HiltViewModel
class MainViewModel @Inject constructor(
application: Application,
private val getGamesUseCase: GetGamesUseCase,
private val getProductsUseCase: GetProductsUseCase
): AndroidViewModel(application) {
private val _games = MutableStateFlow(emptyList<GameItem>())
val games: StateFlow<List<GameItem>> get() = _games

private val _products = MutableStateFlow(emptyList<ProductItem>())
val products: StateFlow<List<ProductItem>> get() = _products

init {
getGames()
getProducts()
}

private fun getGames() {
viewModelScope.launch {
try {
val games = getGamesUseCase()
_games.value = games
} catch (_: Exception) {

}
}
}

private fun getProducts() {
viewModelScope.launch {
try {
val products = getProductsUseCase()
_products.value = products
} catch (_: Exception) {

}
}
}

}

Modify RetrofitModule to include the classes for Product as below

import ai.cloudcnctrai.cloudcnctrai.network.common.Constants
import ai.cloudcnctrai.cloudcnctrai.network.data.GameService
import ai.cloudcnctrai.cloudcnctrai.network.data.ProductApi
import ai.cloudcnctrai.cloudcnctrai.network.data.ProductService
import ai.cloudcnctrai.cloudcnctrai.network.domain.GetGamesUseCase
import ai.cloudcnctrai.cloudcnctrai.network.domain.GetProductsUseCase
import ai.cloudcnctrai.cloudcnctrai.network.network.data.GameApi
import ai.cloudcnctrai.cloudcnctrai.network.repo.GameRepository
import ai.cloudcnctrai.cloudcnctrai.network.repo.ProductRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

@Module
@InstallIn(ViewModelComponent::class)
object RetrofitModule {

@Provides
@ViewModelScoped
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

// Games
@Provides
@ViewModelScoped
fun provideGameApi(retrofit: Retrofit): GameApi {
return retrofit.create(GameApi::class.java)
}

@Provides
@ViewModelScoped
fun provideGameService(gameApi: GameApi): GameService {
return GameService(gameApi) // Or however you create an instance of GameService
}


@Provides
@ViewModelScoped
fun provideGameRepository(gameService: GameService): GameRepository {
return GameRepository(gameService) // Or however you create an instance of GameRepository
}

@Provides
@ViewModelScoped
fun provideGamesUseCase(gameRepository: GameRepository): GetGamesUseCase {
return GetGamesUseCase(gameRepository) // Or however you create an instance of GetGamesUseCase
}

// Products
@Provides
@ViewModelScoped
fun provideProductApi(retrofit: Retrofit): ProductApi {
return retrofit.create(ProductApi::class.java)
}


@Provides
@ViewModelScoped
fun provideProductService(productApi: ProductApi): ProductService {
return ProductService(productApi)
}


@Provides
@ViewModelScoped
fun provideProductRepository(productService: ProductService): ProductRepository {
return ProductRepository(productService)
}

@Provides
@ViewModelScoped
fun provideProductsUseCase(productRepository: ProductRepository): GetProductsUseCase {
return GetProductsUseCase(productRepository)
}

}

Implement the TabThreeScreen composable to use the products emitted by MainViewModel as below


import ai.cloudcnctrai.cloudcnctrai.network.domain.item.GameItem
import ai.cloudcnctrai.cloudcnctrai.network.domain.item.ProductItem
import ai.cloudcnctrai.cloudcnctrai.theme.DeepBlue
import ai.cloudcnctrai.cloudcnctrai.theme.PrimaryColor
import ai.cloudcnctrai.cloudcnctrai.viewmodel.MainViewModel
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
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.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.rememberImagePainter

@Composable
fun TabThreeScreen(
mainViewModel: MainViewModel = hiltViewModel()
) {
val products by mainViewModel.products.collectAsState()
Box(
modifier = Modifier
.fillMaxSize()
.background(PrimaryColor),
contentAlignment = Alignment.Center
) {
LazyColumn {
items(products) {productItem: ProductItem ->
ProductCard(productItem = productItem)
}
}
}
}

@Composable
fun ProductCard(productItem: ProductItem) {
val image = rememberImagePainter(data = productItem.image)

Card (
elevation = 5.dp,
shape = RoundedCornerShape(5.dp),
modifier = Modifier
.padding(top = 5.dp, bottom = 5.dp, start = 10.dp, end = 10.dp)
.fillMaxSize()
) {
Column {
Image(
painter = image,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(250.dp)
)

Column(modifier = Modifier.padding(10.dp)) {
Text(
text = productItem.title,
fontWeight = FontWeight.Bold
)
Text(
text = productItem.description,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}

@Composable
@Preview
fun TabThreeScreenPreview() {
TabThreeScreen()
}

This gives us the Products screen populated with images, product titles and product descriptions as below

Products screen

The full completed source code for this tutorial can be found in the step2 branch as below

--

--

Arunabh Das
Developers Inc

Sort of an executive-officer-of-the-week of a-techno-syndicalist commune. Cypherpunk, techno-idealist, peacenik, spiritual, humanist