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.
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)
}
}
}
}
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.