Designing a robust, maintainable, and scalable Android application requires implementing solid architecture principles and leveraging modern tools and components. This article provides a comprehensive guide to building an app with MVVM (Model-View-ViewModel) and Clean Architecture using the latest Android components: Coroutines, Hilt, Jetpack Compose, Retrofit, and Gson. We'll use the Star Wars API (https://swapi.dev/api/people/
) as an example.
Why MVVM and Clean Architecture?
- MVVM: Separates UI (View) from business logic (ViewModel) and data (Model), making the codebase more manageable and testable.
- Clean Architecture: Divides the app into layers (Presentation, Domain, and Data) to enforce clear separation of concerns, making the code more reusable and easier to modify.
- Retrofit: A type-safe HTTP client for Android and Java, making it easy to fetch data from a REST API.
- Gson: A library for converting Java objects into JSON and vice versa, which is ideal for handling API responses.
- Jetpack Compose: The modern UI toolkit for building native Android apps with declarative syntax, providing a more intuitive way to design interfaces.
- Hilt: It simplifies the DI process by generating the necessary components at compile-time, allowing us to inject dependencies such as the Retrofit service and the CharacterRepository without manually writing boilerplate code.
App Structure and Folder Format
Here's a sample folder structure for our app:
com.example.starwarsapp
├── data
│ ├── api
│ │ └── StarWarsApiService.kt
│ ├── model
│ │ └── Character.kt
│ ├── repository
│ └── CharacterRepository.kt
├── di
│ └── AppModule.kt
├── domain
│ ├── model
│ │ └── CharacterDomainModel.kt
│ ├── repository
│ │ └── CharacterRepositoryInterface.kt
│ └── usecase
│ └── GetCharactersUseCase.kt
├── presentation
│ ├── ui
│ │ ├── theme
│ │ │ └── Theme.kt
│ │ └── CharacterListScreen.kt
│ └── viewmodel
│ └── CharacterViewModel.kt
└── MainActivity.kt
Step-by-Step Implementation
1. Dependencies in build.gradle
dependencies {
// Retrofit for API requests
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Hilt for dependency injection
implementation 'com.google.dagger:hilt-android:2.48'
kapt 'com.google.dagger:hilt-compiler:2.48'
// Jetpack Compose
implementation 'androidx.compose.ui:ui:1.5.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
// Kotlin Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
2. API Service
StarWarsApiService.kt
package com.example.starwarsapp.data.api
import retrofit2.http.GET
import com.example.starwarsapp.data.model.Character
interface StarWarsApiService {
@GET("people/")
suspend fun getCharacters(): List<Character>
}
3. Model Classes
API Data Model
Character.kt
package com.example.starwarsapp.data.model
data class Character(
val name: String,
val gender: String
)
Domain Model
CharacterDomainModel.kt
package com.example.starwarsapp.domain.model
data class CharacterDomainModel(
val name: String,
val gender: String
)
4. Repository
CharacterRepository.kt
package com.example.starwarsapp.data.repository
import com.example.starwarsapp.data.api.StarWarsApiService
import com.example.starwarsapp.domain.model.CharacterDomainModel
class CharacterRepository(private val apiService: StarWarsApiService) {
suspend fun fetchCharacters(): List<CharacterDomainModel> {
return apiService.getCharacters().map {
CharacterDomainModel(name = it.name, gender = it.gender)
}
}
}
5. Use Case
GetCharactersUseCase.kt
package com.example.starwarsapp.domain.usecase
import com.example.starwarsapp.data.repository.CharacterRepository
import com.example.starwarsapp.domain.model.CharacterDomainModel
class GetCharactersUseCase(private val repository: CharacterRepository) {
suspend operator fun invoke(): List<CharacterDomainModel> {
return repository.fetchCharacters()
}
}
6. ViewModel
CharacterViewModel.kt
package com.example.starwarsapp.presentation.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.starwarsapp.domain.model.CharacterDomainModel
import com.example.starwarsapp.domain.usecase.GetCharactersUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class CharacterViewModel(private val getCharactersUseCase: GetCharactersUseCase) : ViewModel() {
private val _characters = MutableStateFlow<List<CharacterDomainModel>>(emptyList())
val characters: StateFlow<List<CharacterDomainModel>> get() = _characters
init {
fetchCharacters()
}
private fun fetchCharacters() {
viewModelScope.launch {
_characters.value = getCharactersUseCase()
}
}
}
7. Dependency Injection
AppModule.kt
package com.example.starwarsapp.di
import com.example.starwarsapp.data.api.StarWarsApiService
import com.example.starwarsapp.data.repository.CharacterRepository
import com.example.starwarsapp.domain.usecase.GetCharactersUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://swapi.dev/api/")
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
fun provideStarWarsApi(retrofit: Retrofit): StarWarsApiService =
retrofit.create(StarWarsApiService::class.java)
@Provides
fun provideCharacterRepository(apiService: StarWarsApiService) =
CharacterRepository(apiService)
@Provides
fun provideGetCharactersUseCase(repository: CharacterRepository) =
GetCharactersUseCase(repository)
}
8. Compose UI
CharacterListScreen.kt
package com.example.starwarsapp.presentation.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.starwarsapp.presentation.viewmodel.CharacterViewModel
@Composable
fun CharacterListScreen(viewModel: CharacterViewModel) {
val characters = viewModel.characters.collectAsState().value
LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) {
items(characters.size) { index ->
val character = characters[index]
Column(modifier = Modifier.padding(8.dp)) {
Text(text = "Name: ${character.name}")
Text(text = "Gender: ${character.gender}")
}
}
}
}
9. Main Activity
MainActivity.kt
package com.example.starwarsapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.starwarsapp.presentation.ui.CharacterListScreen
import com.example.starwarsapp.presentation.viewmodel.CharacterViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject lateinit var viewModel: CharacterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CharacterListScreen(viewModel = viewModel)
}
}
}
Conclusion
This app architecture demonstrates the seamless integration of MVVM and Clean Architecture principles using modern tools like Compose, Hilt, and Coroutines. By following this pattern, you ensure scalability, testability, and maintainability for your app.
Happy coding!
What’s your favorite Kotlin string manipulation tip? Share in the comments below!