Clean Architecture in the flavour of Jetpack Compose
Paul Allies
Posted on October 14, 2021
By employing clean architecture, you can design applications with very low coupling and independent of technical implementation details, such as databases and frameworks. That way, the application becomes easy to maintain and flexible to change. It also becomes intrinsically testable. Here I’ll show how I structure my clean architecture projects. This time we are going to build an Android todo application using Jetpack Compose. We’ll only illustrate one use case of listing todos retrieved from an API. Let’s get started.
The package structure of the project takes on the following form:
Starting from the bottom,
The PRESENTATION layer will keep all of the UI related code. In this case that would be the view and view model of the list of todos.
The DOMAIN layer keeps all business logic and gives the visitor to the project a good idea of what the code does and not how it does it. In this layer we have:
- UseCases: One file per use case,
- Repository: Repository interfaces
- Model: Business models like Todo which we will reference in business logic use cases and our UI
The DATA layer:
- Repository: Repository implementations
- DataSource: All data source interfaces and data source entities. These entities are different from the domain models, and map directly to request and response objects from the api.
and lastly the CORE layer keep all the components which are common across all layers like constants or configs or dependency injection (which we won’t cover)
Our first task would be always to start with the domain models and data entities
data class Todo(
val id: Int,
val isCompleted: Boolean,
val task: String
)
data class TodoAPIEntity(
val id: Int,
val completed: Boolean,
val title: String
)
fun TodoAPIEntity.toTodo(): Todo {
return Todo(
id = id,
isCompleted = completed,
task = title
)
}
Let’s now write an interface for the TodoDatasource. We need one to enforce how any datasource (api, db, etc) needs to behave.
import za.co.nanosoft.cleantodo.Domain.Model.Todo
interface TodoDataSource {
suspend fun getTodos(): List<Todo>
}
We have enough to write an implementation of this interface and we’ll call it TodoAPIImpl:
interface TodoApi {
@GET("todos")
suspend fun getTodos(): List<TodoAPIEntity>
companion object {
var todoApi: TodoApi? = null
fun getInstance(): TodoApi {
if (todoApi == null) {
todoApi = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build().create(TodoApi::class.java)
}
return todoApi!!
}
}
}
class TodoAPIImpl : TodoDataSource {
override suspend fun getTodos(): List<Todo> {
return TodoAPI.getInstance().getTodos().map { it.toTodo() }
}
}
Note: this repository’s getTodos function returns a list of Todo. So, we have to map TodoEntity -> Todo:
Before we write our TodoRepositoryImpl let’s write the interface for that in the Domain layer
interface TodoRepository {
suspend fun getTodos(): List<Todo>
}
class TodoRepositoryImpl(private val datasource: TodoDataSource) : TodoRepository {
override suspend fun getTodos(): List<Todo> {
return datasource.getTodos()
}
}
We can now see that the TodoRepositoryImpl can take any datasource as a dependency, great for swapping out datasources.
Now that we have our todo repository, we can code up the GetTodos use case
class GetTodos(
private val repository: TodoRepository
) {
suspend operator fun invoke(): List<Todo> {
return repository.getTodos()
}
}
and then in turn we can write our presentation’s view model and view
class TodoViewModel constructor(
private val getTodosUseCase: GetTodos
) : ViewModel() {
private val _todos = mutableStateListOf<Todo>()
val todos: List<Todo>
get() = _todos
suspend fun getTodos() {
viewModelScope.launch {
_todos.addAll(getTodosUseCase())
}
}
}
@Composable
fun TodoListView(vm: TodoViewModel) {
LaunchedEffect(Unit, block = {
vm.getTodos()
})
Scaffold(
topBar = {
TopAppBar(
title = {
Text("Todos")
}
)
},
content = {
Column(modifier = Modifier.padding(16.dp)) {
LazyColumn(modifier = Modifier.fillMaxHeight()) {
items(vm.todos) { todo ->
Row(modifier = Modifier.padding(16.dp)) {
Checkbox(checked = todo.isCompleted, onCheckedChange = null)
Spacer(Modifier.width(5.dp))
Text(todo.task)
}
Divider()
}
}
}
}
)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val vm = TodoViewModel(
getTodosUseCase = GetTodos(
repository = TodoRepositoryImpl(
api = TodoAPIImpl()
)
)
)
super.onCreate(savedInstanceState)
setContent {
CleantodoTheme {
TodoListView(vm)
}
}
}
}
so to recap:
Posted on October 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.