Friday, 7 November 2025

Clean Architecture in Android — Part 4

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.