Introduction to Clean Architecture in Android with Jetpack Compose

Post Avatar

Clean Architecture Introduction

Hi everyone,
It's been a while since I last wrote a post! Today, we’re diving into Clean Architecture — what it is, its advantages and disadvantages, and how you can implement it in your Android apps. While Clean Architecture can be structured using multiple modules, we'll keep it simple with a single app module for learning purposes.

Let's get started.


What is Clean Architecture?
Clean Architecture is a software design pattern based on the principle of Separation of Concerns. Introduced by Robert C. Martin, also known as Uncle Bob, in 2011, it aims to organize code in a way that makes it easy to scale, maintain, and test.

In Clean Architecture, we typically split the project into three main layers: Data, Domain, and Presentation — each responsible for a specific part of the app.


{{image:https://www.danigomez.dev/content/blog/media/introduction_to_clean_architecture_in_android_with_jetpack_compose/1.webp},{size:70%,100%},{position:start}}

Data Layer

The Data layer handles all data operations — whether from a remote server or a local database. It contains models, repositories, database builders, and API service builders.

//[title="src/main/java/post.cleanarchitecture/data/di/NetworkModule.kt"]
@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService =
        retrofit.create(ApiService::class.java)

    @Provides
    @Singleton
    fun provideRepository(apiService: ApiService): Repository =
        RepositoryImpl(apiService)
}
//[title="src/main/java/post.cleanarchitecture/data/repository/RepositoryImpl.kt"]
class RepositoryImpl @Inject constructor(
    private val apiService: ApiService
) : Repository {

    override suspend fun getProductList(): List<ProductItem> {
        return apiService.getAllProductListAPI().map { it.toProductList() }
    }

    override suspend fun getProductDetail(id: String): ProductDetail {
        return apiService.getProductDetailsAPI(id).toProductDetail()
    }
}

Domain Layer

The Domain layer contains the business logic. It includes repository interfaces and use cases, completely isolated from the Data and Presentation layers.

//[title="src/main/java/post.cleanarchitecture/domain/repository/Repository.kt"]
interface Repository {

    suspend fun getProductList(): List<ProductItem>

    suspend fun getProductDetail(id: String): ProductDetail
}
//[title="src/main/java/post.cleanarchitecture/domain/usecase/GetProductListUseCase.kt"]
class GetProductListUseCase @Inject constructor(
    private val repository: Repository
) {

    operator fun invoke(): Flow<UiState<List<ProductItem>>> = flow {
        emit(UiState.Loading())
        try {
            emit(UiState.Success(repository.getProductList()))
        } catch (e: Exception) {
            emit(UiState.Error(e.localizedMessage ?: "An unexpected error occurred"))
        }
    }.flowOn(Dispatchers.IO)
}

Presentation Layer

The Presentation layer handles the UI and user interactions. It includes ViewModels, Fragments, Activities, and Composable functions.

//[title="src/main/java/post.cleanarchitecture/presentation/viewmodel/ProductListViewModel.kt"]
@HiltViewModel
class ProductListViewModel @Inject constructor(
    private val getProductListUseCase: GetProductListUseCase
) : ViewModel() {

    private val _productListState = mutableStateOf(ProductListState())
    val productListState: State<ProductListState> = _productListState

    init {
        getProductListUseCase().onEach { result ->
            when (result) {
                is UiState.Loading -> {
                    _productListState.value = ProductListState(isLoading = true)
                }
                is UiState.Success -> {
                    _productListState.value = ProductListState(data = result.data)
                }
                is UiState.Error -> {
                    _productListState.value = ProductListState(error = result.message)
                }
            }
        }.launchIn(viewModelScope)
    }
}
//[title="src/main/java/post.cleanarchitecture/presentation/screen/ListingScreen.kt"]
@Composable
fun ListingScreen() {
    val viewModel: ProductListViewModel = hiltViewModel()
    val state = viewModel.productListState.value
    val context = LocalContext.current

    when {
        state.isLoading -> {
            Column(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                CircularProgressIndicator(modifier = Modifier.size(50.dp))
            }
        }
        state.data != null -> {
            LazyColumn(modifier = Modifier.fillMaxSize()) {
                items(state.data) { product ->
                    ListItem(product) { clickedProduct ->
                        Toast.makeText(context, clickedProduct.title, Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
        state.error.isNotEmpty() -> {
            Column(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text(text = state.error)
            }
        }
    }
}

{{image:https://www.danigomez.dev/content/blog/media/introduction_to_clean_architecture_in_android_with_jetpack_compose/2.webp},{size:70%,100%},{position:start}}


That’s a simple overview of how Clean Architecture can be applied in Android using Jetpack Compose and Kotlin. By keeping responsibilities separated across layers, you ensure your app is easier to maintain, extend, and test as it grows.