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.GETinterface 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.Injectclass 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/