Building a Real-World Production-Ready App
Welcome to the final part of our Clean Architecture in Android series.
So far, we’ve broken down the layers — domain, data, and presentation — and looked at how responsibilities flow.
Now it’s time to make it real.
In this part, we’ll take everything we’ve learned and apply it to a real project structure, complete with practical advice, coding examples, and deployment insights.
1. Where Clean Architecture Truly Shines
Clean Architecture shines when projects begin to scale.
When you have multiple developers, features, and modules, it ensures that:
- UI changes never break core business logic.
- API migration or database replacement doesn’t force refactors in ViewModels.
- Unit testing is easier and faster.
- Each layer is replaceable — think “plug and play” architecture.
If you’ve ever had to rewrite an app just because APIs changed — Clean Architecture prevents that pain.
2. A Real-World Example — Weather Tracker App
Let’s take a simple, relatable example — a Weather Tracker App that:
- Fetches weather data from a REST API
- Stores the last fetched result in a local Room database
- Displays it in the UI, with offline-first behavior
Folder Structure
com.example.weatherapp/
├── data/
│ ├── api/
│ ├── db/
│ ├── repository/
│ └── model/
├── domain/
│ ├── model/
│ ├── repository/
│ └── usecase/
├── presentation/
│ ├── viewmodel/
│ └── ui/
└── di/
3. Domain Layer — Pure Business Logic
// domain/model/WeatherInfo.kt
data class WeatherInfo(
val temperature: Double,
val humidity: Double,
val description: String
)
// domain/repository/WeatherRepository.kt
interface WeatherRepository {
suspend fun getWeather(city: String): WeatherInfo
}
// domain/usecase/GetWeatherUseCase.kt
class GetWeatherUseCase(private val repository: WeatherRepository) {
suspend operator fun invoke(city: String): WeatherInfo {
return repository.getWeather(city)
}
}
Key Takeaway:
The domain layer is completely independent of frameworks, databases, or APIs.
It defines what the app does, not how it does it.
4. Data Layer — The Implementation Detail
// data/api/WeatherApi.kt
interface WeatherApi {
@GET("weather")
suspend fun getWeather(@Query("q") city: String): WeatherResponse
}
// data/repository/WeatherRepositoryImpl.kt
class WeatherRepositoryImpl(
private val api: WeatherApi,
private val dao: WeatherDao
) : WeatherRepository {
override suspend fun getWeather(city: String): WeatherInfo {
return try {
val apiResult = api.getWeather(city)
val entity = WeatherEntity.fromResponse(apiResult)
dao.insertWeather(entity)
entity.toDomain()
} catch (e: Exception) {
dao.getLastWeather()?.toDomain() ?: throw e
}
}
}
Why it matters:
This layer adapts — it knows how to talk to APIs and databases.
It’s replaceable — tomorrow, you can swap WeatherApi for GraphQL or local JSON without touching the domain.
5. Presentation Layer — ViewModel + UI
// presentation/viewmodel/WeatherViewModel.kt
class WeatherViewModel(
private val getWeatherUseCase: GetWeatherUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<WeatherUIState>()
val uiState = _uiState.asStateFlow()
fun loadWeather(city: String) {
viewModelScope.launch {
_uiState.value = WeatherUIState.Loading
try {
val data = getWeatherUseCase(city)
_uiState.value = WeatherUIState.Success(data)
} catch (e: Exception) {
_uiState.value = WeatherUIState.Error(e.message ?: "Unknown error")
}
}
}
}
The UI simply observes uiState and reacts — no networking or database calls here.
6. Dependency Injection Setup (Hilt Example)
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideWeatherApi(): WeatherApi = Retrofit.Builder()
.baseUrl("https://api.openweathermap.org/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WeatherApi::class.java)
@Provides
fun provideWeatherRepository(api: WeatherApi, dao: WeatherDao): WeatherRepository {
return WeatherRepositoryImpl(api, dao)
}
@Provides
fun provideGetWeatherUseCase(repository: WeatherRepository) =
GetWeatherUseCase(repository)
}
Tip: Never inject the WeatherRepositoryImpl directly into your ViewModel — always inject the interface (WeatherRepository).
That’s what keeps layers clean and testable.
7. Common Mistakes Developers Make
- Putting logic in ViewModels – ViewModels should just coordinate flows, not calculate.
- Coupling API models to UI – Use mappers to convert data models to domain models.
- Skipping the use case layer – This layer is your safety net; it protects business rules.
- Too many abstractions – Don’t abstract for the sake of it. Keep only meaningful boundaries.
8. Testing Made Simple
@Test
fun `GetWeatherUseCase returns correct data`() = runTest {
val fakeRepo = FakeWeatherRepository()
val useCase = GetWeatherUseCase(fakeRepo)
val result = useCase("Delhi")
assertEquals("Clear Sky", result.description)
}
Here, you can test domain logic without Retrofit, Room, or UI — the power of clean boundaries.
9. Real-World Lesson Learned
When we applied Clean Architecture to a production Android app with over 40 screens:
- New feature addition time dropped by 30–40%.
- Bug regression dropped drastically.
- QA cycle shortened because each layer could be tested in isolation.
That’s why even though initial setup feels “heavy”, long-term maintainability pays back many times over.
10. Final Words
You’ve now reached the final stage of mastering Clean Architecture in Android.
If you’ve followed from Part 1, 2, and 3 — you already understand how to think like an architect, not just a developer.
As you build your next project:
- Start small but structured.
- Treat your domain layer as sacred.
- Keep dependencies flowing inward, never outward.
“Clean code isn’t just readable — it’s replaceable.”
💡 Bonus Thoughts
After working across Android, JNI, and multiple frameworks for over a decade, one thing stands out — architecture defines how gracefully your project grows.
Tools and trends change, but architecture decisions stay with your codebase for years.
If you’ve read this far, you’re already among the 10% of developers who think about how to build, not just what to build.
Keep learning, keep refactoring, and remember — the best architecture is the one your entire team understands.
— Written by a developer who believes clean code is an act of kindness to your future self.
