We will build simple app using jsonplaceholderAPI for this project and use Retrofit library with MVVM in JetPack Compose.
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.