Retrofit with MVVM in Jetpack Compose

Retrofit with MVVM in Jetpack Compose

·

6 min read

We will build simple app using jsonplaceholderAPI for this project and use Retrofit library with MVVM in JetPack Compose.

💡
If you are looking for Jetpack Compose ROOMDB Setup with Flow, DI and MVVM : Article : Compose MVVM RoomDB with Flow and DI

So here are the files.

Dependencies:

Project Level Build.gradle:

buildscript {
    ext {
        compose_ui_version = '1.1.1'
        hilt_version = '2.39.1'
        room_version = "2.4.3"
        accompanist_version = '0.25.0'
        lifecycle_version = "2.6.0-alpha01"
    }
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.3.1' apply false
    id 'com.android.library' version '7.3.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
    id 'com.google.dagger.hilt.android' version '2.41' apply false
}

App Level Build.gradle:

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

android {
    namespace 'com.developersmarket.componentscompose'
    compileSdk 32

    defaultConfig {
        applicationId "com.developersmarket.componentscompose"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.1.1'
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    implementation "androidx.compose.ui:ui:$compose_ui_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
    implementation 'androidx.compose.material:material:1.1.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"


    //Room
    implementation "androidx.room:room-ktx:$room_version"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

    //retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    //moshi
    implementation("com.squareup.moshi:moshi-kotlin:1.12.0")
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

    //dagger hilt
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"

    kapt("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.4.2")

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

    // android ktx
    implementation 'androidx.activity:activity-ktx:1.5.1'

    //hilt viewmodel
//    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'

    //pager
    implementation "com.google.accompanist:accompanist-pager:$accompanist_version"
    implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version"

    //navigation
    implementation("androidx.navigation:navigation-compose:2.5.1")


    // Local unit tests
    testImplementation "androidx.test:core:1.4.0"
    testImplementation "junit:junit:4.13.2"
    testImplementation "androidx.arch.core:core-testing:2.1.0"
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
    testImplementation "com.google.truth:truth:1.1.3"
    testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
    debugImplementation "androidx.compose.ui:ui-test-manifest:1.2.1"

    // Instrumentation tests
    androidTestImplementation 'com.google.dagger:hilt-android-testing:2.41'
    kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42'
    androidTestImplementation "junit:junit:4.13.2"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
    androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
    androidTestImplementation "com.google.truth:truth:1.1.3"
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test:core-ktx:1.4.0'
    androidTestImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
    androidTestImplementation 'androidx.test:runner:1.4.0'

}

File Structure:

Create Necessary Packages As needed.

Lets Start with

BaseApplication.kt:

package com.developersmarket.componentscompose

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class BaseApplication: Application() {}

In AndroidManifest Declare it as name like below :

android:name=".BaseApplication"

Complete AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:name=".BaseApplication"
        android:theme="@style/Theme.ComponentsCompose"
        tools:targetApi="31">
        <activity
            android:name=".retrofit.ui.RetrofitActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.ComponentsCompose">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>

// 
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.ComponentsCompose">
<!--            <intent-filter>-->
<!--                <action android:name="android.intent.action.MAIN" />-->

<!--                <category android:name="android.intent.category.LAUNCHER" />-->
<!--            </intent-filter>-->

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

Note Above MainActivity is Not used we will be using RetrofitActivity.

Retrofit Setup:

network/ApiService.kt:

package com.developersmarket.componentscompose.retrofit.network

import com.developersmarket.componentscompose.retrofit.Post
import retrofit2.http.GET

interface ApiService {

    companion object {
        const val BASE_URL = "https://jsonplaceholder.typicode.com/"
    }

    @GET("posts")
    suspend fun getPosts(): List<Post>
}

di/AppModule.kt:

package com.developersmarket.componentscompose.retrofit.di

import com.developersmarket.componentscompose.retrofit.network.ApiService
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton


@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides @Singleton fun provideMoshi(): Moshi =
        Moshi.Builder().add(KotlinJsonAdapterFactory()).build()

    @Provides @Singleton fun providesApiService(moshi: Moshi): ApiService =
        Retrofit.Builder()
            .run {
                baseUrl(ApiService.BASE_URL)
                    .addConverterFactory(MoshiConverterFactory.create(moshi))
                    .build()
            }.create(ApiService::class.java)

}

MainRepository.kt:

package com.developersmarket.componentscompose.retrofit

import com.developersmarket.componentscompose.retrofit.network.ApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject

class MainRepository
@Inject
constructor(private val apiService: ApiService){
    fun getPost(): Flow<List<Post>> = flow {
        emit(apiService.getPosts())
    }.flowOn(Dispatchers.IO)
}

Utils

util/DataSource.kt:

package com.developersmarket.componentscompose.util

data class User(
        val description: String
)

fun dummyData():List<User>{
    return listOf(

            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
            User("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),


    )
}

util.ApiState:

package com.developersmarket.componentscompose.util

import com.developersmarket.componentscompose.retrofit.Post

sealed class ApiState {
    class Success(val data: List<Post>) : ApiState()
    class Failure(val msg: Throwable) : ApiState()
    object Loading:ApiState()
    object Empty: ApiState()
}

ViewModel:

ui/MainViewModel.kt

package com.developersmarket.componentscompose.retrofit.ui

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.developersmarket.componentscompose.retrofit.MainRepository
import com.developersmarket.componentscompose.util.ApiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class MainViewModel
@Inject constructor(private val mainRepository: MainRepository) : ViewModel() {
    val response: MutableState<ApiState> = mutableStateOf(ApiState.Empty)

    init {
        getPost()
    }
    fun getPost() =
        viewModelScope.launch {
            mainRepository.getPost().onStart {
                response.value= ApiState.Loading
            }.catch {
                response.value= ApiState.Failure(it)
            }.collect {
                response.value=ApiState.Success(it)
            }
        }
}

Final Activity:

RetrofitActivity.kt

package com.developersmarket.componentscompose.retrofit.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxWidth
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.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.developersmarket.componentscompose.retrofit.Post
import com.developersmarket.componentscompose.ui.theme.ComponentsComposeTheme
import com.developersmarket.componentscompose.util.ApiState
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class RetrofitActivity : ComponentActivity() {
    private val mainViewModel: MainViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComponentsComposeTheme() {
                Surface(color = MaterialTheme.colors.background) {
                    GETData(mainViewModel)
                }
            }
        }
    }

    @Composable fun EachRow(post: Post) {
        Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(
                            horizontal = 8.dp,
                            vertical = 8.dp
                    ),
                elevation = 2.dp,
                shape = RoundedCornerShape(4.dp)
        ) {
            Text(
                    text = post.body,
                    modifier = Modifier.padding(10.dp)
            )
        }

    }

    @Composable fun GETData(mainViewModel: MainViewModel) {
        when (val result = mainViewModel.response.value) {
            is ApiState.Success -> {
                LazyColumn {
                    items(result.data) { response ->
                        EachRow(post = response)
                    }
                }
            }

            is ApiState.Failure -> {
                Text(text = "${result.msg}")
            }

            ApiState.Loading -> {
                CircularProgressIndicator()
            }

            ApiState.Empty -> {

            }
        }
    }
}

Thats It :)

Output:

Source Code:

https://github.com/saurabhthesuperhero/ComposeRetrofit

if you find Something isnt working do check imports properly sometime wrong imports cause all the problems, yet all the best, and thanks for reading.

Did you find this article valuable?

Support saurabh jadhav by becoming a sponsor. Any amount is appreciated!