← Back to Articles
Android Kotlin Clean Architecture MVVM Software Architecture Mobile Development

Android Clean Architecture with Kotlin: Complete Guide for Scalable Android Applications

Learn how to build scalable Android applications using Clean Architecture with Kotlin. Understand layers, dependency rules, MVVM, use cases, repositories, dependency injection, testing, and modern Android development practices.

July 2026
35 min read
Written by Engineering Team

Introduction

Modern Android applications are becoming increasingly complex. Features such as authentication, offline support, payments, notifications, and real-time synchronization require an architecture that can scale as the application grows.

Without proper architecture, Android projects often become difficult to maintain. UI components start containing business logic, network code becomes mixed with database operations, and testing becomes complicated.

Clean Architecture provides a structured approach by separating responsibilities into independent layers. Combined with Kotlin, Jetpack Compose, Coroutines, Flow, and dependency injection, it allows developers to create maintainable and testable Android applications.

Android Clean Architecture layers
Clean Architecture separates presentation, domain, and data responsibilities.

What Is Clean Architecture?

Clean Architecture is a software design approach introduced by Robert C. Martin that focuses on separation of concerns and dependency management.

The main idea is that business rules should not depend on frameworks, databases, or external systems.

Problem Clean Architecture Solution
Business logic inside Activities Move logic into ViewModels and Use Cases
Database dependency everywhere Use repository abstraction
Hard testing Separate independent layers
Changing frameworks Keep core logic independent

Clean Architecture Dependency Rule

The most important rule of Clean Architecture is that dependencies should always point toward the domain layer.

The inner layers should not know anything about the outer layers.

  • Domain should not depend on Android framework

  • Domain should not depend on databases

  • Business rules should be independent

  • External implementations depend on abstractions

Android Clean Architecture Layers

Layer Responsibility
Presentation UI, ViewModel, UI State
Domain Business rules and use cases
Data API, database, repositories

Presentation Layer

The presentation layer contains everything related to displaying information and handling user interactions.

  • Jetpack Compose screens

  • Activities and Fragments

  • ViewModels

  • UI state models

  • User events

The presentation layer should not contain business rules. Its responsibility is to display state and send user actions to the ViewModel.

ViewModel Example

UserViewModel.kt
kotlin
              class UserViewModel(
    private val getUsersUseCase: GetUsersUseCase
) : ViewModel() {


    private val _state =
        MutableStateFlow(
            UserState()
        )


    val state =
        _state.asStateFlow()



    fun loadUsers() {

        viewModelScope.launch {

            val users =
                getUsersUseCase()


            _state.value =
                UserState(
                    users = users
                )

        }

    }

}

            

UI State Management

Modern Android applications should represent the complete screen state using a single state object.

UserState.kt
kotlin
              data class UserState(

    val loading: Boolean = false,

    val users: List<User> = emptyList(),

    val error: String? = null

)

            

Domain Layer

The domain layer contains the core business logic of the application. It should be independent from Android frameworks and external technologies.

  • Use Cases

  • Business rules

  • Domain models

  • Repository interfaces

Use Cases

Use Cases represent individual business operations. They provide a clean entry point between the ViewModel and business logic.

GetUsersUseCase.kt
kotlin
              class GetUsersUseCase(

    private val repository: UserRepository

) {


    suspend operator fun invoke()
        : List<User> {


        return repository.getUsers()

    }

}

            

Repository Interfaces

The domain layer defines repository interfaces while the data layer provides their implementations.

UserRepository.kt
kotlin
              interface UserRepository {


    suspend fun getUsers()
        : List<User>


}

            

Data Layer

The data layer is responsible for providing application data. It communicates with external sources such as REST APIs, local databases, files, and other data providers.

The data layer implements repository interfaces defined in the domain layer. This keeps the business logic independent from specific technologies.

  • Remote data sources

  • Local data sources

  • Repository implementations

  • API models

  • Database entities

Data Layer Structure

Project Structure
text
              data/

├── remote/

│   ├── ApiService.kt

│   └── UserDto.kt


├── local/

│   ├── UserDao.kt

│   └── UserEntity.kt


├── repository/

│   └── UserRepositoryImpl.kt

            

Remote Data Source with Retrofit

Retrofit is commonly used for communicating with backend APIs. The remote data source isolates network operations from the rest of the application.

UserApi.kt
kotlin
              interface UserApi {


    @GET("users")

    suspend fun getUsers()
        : List<UserDto>


}

            

DTO Models

Data transfer objects represent data received from external systems. They should not be used directly inside the domain layer.

UserDto.kt
kotlin
              data class UserDto(

    val id: Long,

    val name: String

)

            

Mapping Data Models

Mapping converts external models into domain models. This prevents external changes from affecting business logic.

UserMapper.kt
kotlin
              fun UserDto.toDomain()
    : User {


    return User(

        id = id,

        name = name

    )

}

            

Repository Implementation

The repository combines different data sources and provides data according to domain requirements.

UserRepositoryImpl.kt
kotlin
              class UserRepositoryImpl(

    private val api: UserApi

) : UserRepository {


    override suspend fun getUsers()
        : List<User> {


        return api.getUsers()
            .map {
                it.toDomain()
            }

    }

}

            

Room Database Integration

Room provides a local database solution for Android applications. In Clean Architecture, Room belongs inside the data layer.

UserDao.kt
kotlin
              @Dao

interface UserDao {


    @Query(
        "SELECT * FROM users"
    )

    fun observeUsers()
        : Flow<List<UserEntity>>

}

            

Offline First Architecture

Modern Android applications often use an offline-first approach where local storage becomes the primary source of data.

The application reads from the database and synchronizes changes with the server when network connectivity is available.

  • Better user experience

  • Works without internet

  • Faster loading times

  • Reduced network usage

Repository with Local and Remote Sources

UserRepositoryImpl.kt
kotlin
              class UserRepositoryImpl(

    private val api: UserApi,

    private val dao: UserDao

) : UserRepository {


    override fun getUsers()
        : Flow<List<User>> {


        return dao.observeUsers()
            .map {
                users ->
                users.map {
                    it.toDomain()
                }
            }

    }

}

            

Dependency Injection with Hilt

Dependency injection allows classes to receive their dependencies instead of creating them internally. Hilt is the recommended dependency injection framework for Android.

  • Improved testability

  • Loose coupling

  • Cleaner architecture

  • Simpler dependency management

Providing Repository Dependencies

RepositoryModule.kt
kotlin
              @Module

@InstallIn(
    SingletonComponent::class
)

object RepositoryModule {


    @Provides

    fun provideUserRepository(

        api: UserApi

    ): UserRepository {


        return UserRepositoryImpl(
            api
        )

    }

}

            

Complete Android Project Structure

Clean Architecture Project
text
              app/


presentation/

├── screens/

├── components/

├── viewmodel/


domain/

├── model/

├── usecase/

├── repository/


data/

├── api/

├── database/

├── repository/


di/

├── NetworkModule.kt

├── DatabaseModule.kt

            

Multi Module Clean Architecture

Large Android applications often separate code into multiple Gradle modules. This improves build performance, ownership, and scalability.

Multi Module Structure
text
              :app


:core

:core:network

:core:database


:domain


:feature:profile

:feature:settings

:feature:authentication

            

Benefits of Multi Module Architecture

  • Faster builds

  • Better code organization

  • Independent feature development

  • Reusable modules

  • Clear ownership boundaries

Testing Clean Architecture

One of the biggest advantages of Clean Architecture is improved testability. Each layer can be tested independently.

Layer Testing Approach
Domain Unit tests for use cases
Presentation ViewModel and UI tests
Data Repository and API tests

Testing a Use Case

GetUsersUseCaseTest.kt
kotlin
              @Test

fun shouldReturnUsers() = runTest {


    val result =
        useCase()


    assertEquals(
        3,
        result.size
    )

}

            

Integrating Clean Architecture with Jetpack Compose

Jetpack Compose works naturally with Clean Architecture because composable functions focus only on displaying state while ViewModels handle business communication.

The UI layer observes state exposed by the ViewModel and sends user actions back without knowing how data is loaded or stored.

UserScreen.kt
kotlin
              @Composable
fun UserScreen(

    viewModel: UserViewModel

) {


    val state by viewModel.state
        .collectAsState()


    when {

        state.loading -> {

            CircularProgressIndicator()

        }


        else -> {

            UserList(
                state.users
            )

        }

    }

}

            

Handling User Events

A common Compose architecture pattern is unidirectional data flow. Events move from the UI to the ViewModel, and state moves from the ViewModel back to the UI.

UserEvent.kt
kotlin
              sealed interface UserEvent {


    data object Refresh
        : UserEvent


    data class Delete(
        val id: Long
    ) : UserEvent

}

            
ViewModelEvents.kt
kotlin
              fun onEvent(
    event: UserEvent
) {

    when(event) {

        UserEvent.Refresh -> {

            loadUsers()

        }

    }

}

            

Feature Based Package Structure

For larger applications, organizing code by feature is usually better than organizing only by technical layer.

Feature Structure
text
              features/


authentication/

    data/

    domain/

    presentation/


profile/

    data/

    domain/

    presentation/


settings/

    data/

    domain/

    presentation/

            

Feature-based architecture makes it easier for teams to work independently and prevents unrelated features from becoming tightly coupled.

Error Handling Strategy

Production applications need a consistent approach for handling errors from APIs, databases, and business operations.

ResultWrapper.kt
kotlin
              sealed class Result<T> {


    data class Success<T>(
        val data: T
    ) : Result<T>()


    data class Error<T>(
        val message: String
    ) : Result<T>()

}

            

Network Error Handling

Repositories should convert technical errors into meaningful domain results before passing them to the UI layer.

RepositoryError.kt
kotlin
              override suspend fun login()
: Result<User> {


    return try {


        val user =
            api.login()


        Result.Success(
            user
        )


    }
    catch(e: Exception) {


        Result.Error(
            "Login failed"
        )

    }

}

            

Loading, Success, and Error States

A predictable UI state model makes applications easier to maintain and test.

ScreenState.kt
kotlin
              sealed interface ScreenState {


    data object Loading
        : ScreenState


    data class Success(
        val data: User
    ) : ScreenState


    data class Error(
        val message: String
    ) : ScreenState

}

            

Clean Architecture vs MVVM

MVVM and Clean Architecture are often confused because they solve different problems. MVVM is a presentation pattern, while Clean Architecture defines the overall application structure.

MVVM Clean Architecture
Presentation pattern Complete architecture approach
Defines ViewModel usage Defines dependency direction
Handles UI organization Handles business separation
Can exist alone Often uses MVVM inside presentation

Recommended Modern Android Stack

Purpose Technology
UI Jetpack Compose
State StateFlow
Async Kotlin Coroutines
Architecture Clean Architecture
Dependency Injection Hilt
Networking Retrofit
Database Room
Testing JUnit + Compose Testing

Common Clean Architecture Mistakes

  • Creating unnecessary abstractions

  • Putting business logic inside ViewModels

  • Using database entities everywhere

  • Making repositories too large

  • Ignoring dependency direction

  • Creating too many tiny use cases

  • Skipping testing

When Should You Use Clean Architecture?

Clean Architecture is especially useful for medium and large applications where long-term maintainability is important.

  • Enterprise applications

  • Applications with multiple developers

  • Apps requiring extensive testing

  • Long-lived products

  • Complex business rules

For very small applications, a simpler architecture may be enough. The goal is not adding complexity, but creating boundaries where complexity already exists.

Frequently Asked Questions

Is Clean Architecture required for every Android app?
No. Small applications may not need all layers. Clean Architecture is most valuable when applications become complex and require long-term maintenance.
Does Clean Architecture make applications slower?
No. The additional layers mainly improve organization and testing. Runtime performance depends on implementation details, not architecture style.
Should every repository have a use case?
Not always. Use cases are valuable when they represent meaningful business operations. Simple data retrieval may not require additional abstraction.
Can Clean Architecture work with Jetpack Compose?
Yes. Compose works very well with Clean Architecture because UI becomes a simple observer of state exposed by ViewModels.
Is Clean Architecture only for Android?
No. The principles can be applied to backend systems, web applications, desktop applications, and other software platforms.

Conclusion

Clean Architecture provides a strong foundation for building scalable Android applications with Kotlin. By separating presentation, business logic, and data responsibilities, developers can create applications that are easier to test, maintain, and extend. Combined with Jetpack Compose, Coroutines, Flow, and modern dependency injection, Clean Architecture represents the recommended approach for professional Android development.

We use cookies to improve your experience.