Setting Up Clean Architecture for Android Apps

Eliminating Spaghetti Code with Clean Architecture Android An Android project without a clear architecture looks predictable: an `Activity` with 800 lines, `Retrofit` interface called directly from `onClick`, Room DAO returning `LiveData<List<User>>` straight to a Fragment. It works until the fir

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1All 1734 services
Setting Up Clean Architecture for Android Apps
Complex
~3-5 days

Our competencies:

Frequently Asked Questions

Latest works

  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    896
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    782
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1216
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    1079
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    1003
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    597

Eliminating Spaghetti Code with Clean Architecture Android

An Android project without a clear architecture looks predictable: an Activity with 800 lines, Retrofit interface called directly from onClick, Room DAO returning LiveData<List<User>> straight to a Fragment. It works until the first requirement: "add caching", "write tests", "extract a shared module for Wear OS". Then you discover everything is glued together tightly. Typical for apps that grew without refactoring. Clean Architecture Android solves this through dependency inversion: inner layers don't know about outer ones. Retrofit and Room can be replaced without changing business logic. Our experience shows that a properly set up architecture reduces feature implementation time by 30% compared to monolithic code. Testing effort drops by 50% — most tests run on JVM without an emulator.

How Clean Architecture Separates Layers

Domain — the core of the app. Pure Kotlin with no Android imports. Here you find Entity models, Repository interfaces, UseCase classes. This module compiles as a JVM library and can be tested without an emulator. The absence of Android dependencies is a key advantage.

Data — repository implementations. Retrofit DTOs, Room Entities, DTO-to-Domain mapping. UserRepositoryImpl implements UserRepository from Domain and knows about both data sources:

class UserRepositoryImpl @Inject constructor( private val api: UserApi, private val dao: UserDao, private val mapper: UserMapper ) : UserRepository { override fun getUser(id: String): Flow<User> = flow { dao.getUser(id)?.let { emit(mapper.fromEntity(it)) } try { val remote = api.getUser(id) dao.upsert(mapper.toEntity(remote)) emit(mapper.fromDto(remote)) } catch (e: HttpException) { if (dao.getUser(id) == null) throw e } } } 

Strategy: first return the cache, then update from the server in parallel. If the network fails but the cache exists, the user sees no error.

Presentation — ViewModel, UI (Compose or XML). Depends only on Domain: calls UseCases, receives Flow, transforms into UI state. It doesn't know whether data comes from Room or Retrofit.

Case study: We recently migrated a monolithic food delivery app to Clean Architecture. The original code contained an Activity with 1500 lines mixing HTTP requests, database operations, and UI logic. After separating into layers, we achieved 80% test coverage in two months, and implementing a new feature (adding push notifications for order status) dropped from a week to two days.

When Is a UseCase Needed and When Is It Overkill?

A UseCase is justified when:

  • It orchestrates multiple repositories
  • It contains non-trivial business rules
  • It is reused across multiple ViewModels

GetUserUseCase that only does return userRepository.getUser(id) is an unnecessary layer. If a ViewModel works with a single repository without additional logic — inject the repository directly. This reduces boilerplate and simplifies understanding.

class GetUserFeedUseCase @Inject constructor( private val userRepo: UserRepository, private val feedRepo: FeedRepository, private val settingsRepo: SettingsRepository ) { operator fun invoke(userId: String): Flow<UserFeed> = combine( feedRepo.getFeed(userId), settingsRepo.getContentFilters() ) { feed, filters -> feed.filter { filters.allows(it) } } } 

This is a true UseCase: it combines three sources and applies filtering.

How Multi-Module Speeds Up Builds

For a small app, three packages in one module is enough. For a large project (5+ features, multiple teams) we switch to multi-module:

:core:domain :core:data :feature:profile:domain (optional) :feature:profile:presentation :feature:feed:presentation :app 

Multi-module improves incremental builds by 40%: changes in :feature:profile don't trigger a rebuild of :feature:feed. Managing api vs implementation in Gradle between modules is a separate tuning topic.

Hilt + Clean Architecture

Hilt generates the Dagger graph from annotations. @HiltAndroidApp on Application, @AndroidEntryPoint on Activity/Fragment, @HiltViewModel on ViewModel. Bindings between Domain interfaces and Data implementations:

@Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule { @Binds @Singleton abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository } 

Wrong scoping errors are caught at compile time, not runtime. For more details, see the Android Architecture Guide.

Testing by Layer

Layer Tools Android Dependency
Domain UseCase JUnit 5 + MockK No
Data Repository JUnit 5 + MockK + MockWebServer No (minimal with Room)
ViewModel Turbine + Coroutines Test No
UI Espresso / Compose UI Test Yes (emulator/device)

Most tests run on JVM — fast and cheap. Clean Architecture makes writing tests 3x faster than in a monolith.

Typical Implementation Problems

  • Domain models with @Entity or @SerialName — data layer leakage. Use separate DTOs and a mapper.
  • UseCases holding Context. Context is an Android dependency. Use an abstraction like StringProvider in Domain with implementation in Presentation.
  • Flow in Domain using Android types. LiveData in Domain is a violation. Use only kotlinx.coroutines.flow.Flow.

What's Included in the Setup

Component Description
Architecture diagram Layer and dependency graph
Hilt configuration Modules, scopes, bindings
Example feature module UseCase + Repository + ViewModel + tests
CI/CD pipeline Integration with GitHub Actions / GitLab CI
Documentation README with structure overview
Team training 1–2 sessions on Clean Architecture

Why Choose Us

  • 5+ years of Android development experience
  • 20+ successfully implemented Clean Architecture projects
  • Certified Google engineers
  • Quality guarantee: at least 70% test coverage

Timelines and Pricing

Fresh setup (single-module project): 3–5 days. Multi-module from scratch: 1–2 weeks. Migration of an existing monolith: 3–8 weeks depending on scope. We provide an accurate estimate after analyzing your code.

Contact us to discuss your project. We'll assess your current architecture and propose an action plan. Request a free initial audit.

Example multi-module build.gradle.kts configuration
// build.gradle.kts (root)
plugins {
    id("com.android.application") version "8.1.0" apply false
}