Pandiyan Mani
5 min readSep 16, 2022

Clean Architecture Using MVVM

Hi today we are going to see an topic on MVVM, Use Case, Jetpack Compose, Hilt and Retrofit.

So first we can start by structuring our folder inside the Android Studio

We are going to create five main packages

Data → Which hold the Remote API/Database Stuff, and dto(Data Transfer Object) and where we add the original Repository Implementation.

Domain → Which holds model class, Use Cases and Repository

DI → Which provides the Dependency Injection for the Required ones.

Presentation → which holds view and viewmodels.

Common → which hold sealed class for Api Result or Constant values or things which is used all over the project.

Below is the screen shot of the folder structure

We need to add the dependencies needed for our project under the app level build gradle

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {// Compose dependencies
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07"
implementation "androidx.navigation:navigation-compose:2.4.0-alpha08"
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.3.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
//Dagger - Hilt
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-android-compiler:2.37"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
kapt "androidx.hilt:hilt-compiler:1.0.0"
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0-alpha03'
// 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"
}

And on the top level build gradle file we need to add the hilt plugin

classpath "com.google.dagger:hilt-android-gradle-plugin:2.38.1"

So the first step is to create a Resource Sealed Class under Common Folder which handles the network result.

So we created 3 classes inside Resource Sealed class one is for handling success response and another one is for failure and another one is for loading status

package com.swigy.cleanarchitecture.commonsealed class Resource(
val data: T? = null,
val message: String? = null
) {
class Success(data: T): Resource(data)
class Error(
message: String? = null,
data: T? = null
): Resource(data, message)
class Loading(data: T? =null): Resource(data)
}

Next I created an Constant Object under Common Folder for accessing things like baseUrl or any constant values.

object Constant {
val baseUrl = "https://jsonplaceholder.typicode.com/"
}

So now I am moving the dto folder inside remote package to keep that all Api operation on a single package

Under remote package creating an ApiInterface which hold the endpoints with ssuspend function

package com.swigy.cleanarchitecture.data.remoteimport com.swigy.cleanarchitecture.data.remote.dto.userDetailsItemdto
import retrofit2.http.GET
interface ApiInterface { @GET("comments?postId=1")
suspend fun getUserDetails(): List
}

Since we need the response in the form of List we are creating a data class under dto package as userDetailsItemdto which holds the response data

package com.swigy.cleanarchitecture.data.remote.dtodata class userDetailsItemdto(
val body: String,
val email: String,
val id: Int,
val name: String,
val postId: Int
)

Since we need to display only the email, name and body in the list for that i am creating an data class under Domain package inside model folder as userDetailsItem

package com.swigy.cleanarchitecture.domain.modeldata class userDetailsItem(
val body: String,
val email: String,
val name: String
)

Now we need to map this on the dto of userDetailsItemdto to userDetailsItem inside the userDetailsItemdto data class i am just creating an function to map only the needed data

package com.swigy.cleanarchitecture.data.remote.dtoimport com.swigy.cleanarchitecture.domain.model.userDetailsItemdata class userDetailsItemdto(
val body: String,
val email: String,
val id: Int,
val name: String,
val postId: Int
)
fun userDetailsItemdto.touserDetailsItem(): userDetailsItem {
return userDetailsItem(
body = body,
email = email,
name = name
)
}

there are two repository we have one under data package and domain package so data package repository contains the actual implementation and the one under domain describe the function we going to do so with this repository its easy for us to right the test class since we don’t need to actual implementation which will take long time.

lets first create a NetworkRepositary under repositary folder of domain package

package com.swigy.cleanarchitecture.data.repositaryimport com.swigy.cleanarchitecture.domain.model.userDetailsIteminterface NetworkRepositary {    suspend fun getUserDetails(): List
}

Next create NetworkRepositaryImpl under repositary folder of data package

package com.swigy.cleanarchitecture.data.repositaryimport com.swigy.cleanarchitecture.data.remote.ApiInterface
import com.swigy.cleanarchitecture.data.remote.dto.userDetailsItemdto
import com.swigy.cleanarchitecture.domain.repositary.NetworkRepositary
import javax.inject.Inject
class NetworkRepositaryImpl @Inject constructor(
val apiInterface: ApiInterface
): NetworkRepositary {
override suspend fun getUserDetails(): List {
return apiInterface.getUserDetails()
}
}

So under usecase package created a class called GetUserDetails

package com.swigy.cleanarchitecture.domain.usecaseimport com.swigy.cleanarchitecture.common.Resource
import com.swigy.cleanarchitecture.data.remote.dto.touserDetailsItem
import com.swigy.cleanarchitecture.domain.model.userDetailsItem
import com.swigy.cleanarchitecture.domain.repositary.NetworkRepositary
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
class GetUserDetails @Inject constructor(
val networkRepositary: NetworkRepositary
) {
operator fun invoke(): Flow>> = flow {
try {
val result = networkRepositary.getUserDetails().map { it.touserDetailsItem() }
emit(Resource.Success>(result))
} catch (e: Exception) {
emit(Resource.Error>("Connection Failed"))
}
}
}

which gets the result and emit back to viewModel

Next we need to provide the dependency so inside the di package I am creating an object named AppModule

package com.swigy.cleanarchitecture.diimport com.swigy.cleanarchitecture.common.Constant
import com.swigy.cleanarchitecture.data.remote.ApiInterface
import com.swigy.cleanarchitecture.data.repositary.NetworkRepositaryImpl
import com.swigy.cleanarchitecture.domain.repositary.NetworkRepositary
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModules {
@Provides
@Singleton
fun getApiInterface():ApiInterface {
return Retrofit.Builder()
.baseUrl(Constant.baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiInterface::class.java)
}
@Provides
@Singleton
fun getNetworkRepositary(apiInterface: ApiInterface):NetworkRepositary {
return NetworkRepositaryImpl(apiInterface)
}
}

the @InstallIn(SingletonComponent::class) define the lifecycle of the component it live as long as app lives

@Singleton return the same instance each time its called for

In order to call the AppModules we create a an application class

package com.swigy.cleanarchitectureimport android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MVVMApplication: Application()

and declare it on manifest under appliccation

android:name=".MVVMApplication"
android:allowBackup="true"

And @HiltAndroidApp will create the component needed for Injection at the start

we create a viewModel class where we get data and pass it to view

package com.swigy.cleanarchitecture.presentationimport androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.swigy.cleanarchitecture.common.Resource
import com.swigy.cleanarchitecture.domain.model.userDetailsItem
import com.swigy.cleanarchitecture.domain.model.userListState
import com.swigy.cleanarchitecture.domain.usecase.GetUserDetails
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@HiltViewModel
class userDetailsViewModel @Inject constructor(private val getUserDetails: GetUserDetails): ViewModel() {
private val _state = MutableLiveData()
var state: LiveData = _state
init {
getuserDetails()
}
fun getuserDetails() {
getUserDetails().onEach {
when(it) {
is Resource.Success -> {
_state.value = userListState(userDetails = it.data)
}
is Resource.Error -> {
_state.value = userListState(message = it.message ?: "An unexpected error occured")
}
is Resource.Loading -> {

}
}
}
}
}

Inside the MainActivity we listen for the changes from viewModel

package com.swigy.cleanarchitecture.presentationimport android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.swigy.cleanarchitecture.presentation.theme.CleanArchitectureTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CleanArchitectureTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val userDetailsViewModel:userDetailsViewModel = hiltViewModel()
userDetailsViewModel.getuserDetails()
userDetailsViewModel.state.observe(this) {
it
.userDetails?.let {
for(element in it) {
Toast.makeText(this, ""+ element.name, Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
}
}

Source code available on below link

https://bitbucket.org/PandiyanMani100/cleanarchitecturemvvm/src/master/

No responses yet