DEV Community

Cover image for Clean Architecture in the flavour of Jetpack Compose
Paul Allies
Paul Allies

Posted on • Updated on

Clean Architecture in the flavour of Jetpack Compose

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:

Package Structure

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:

  1. UseCases: One file per use case,
  2. Repository: Repository interfaces
  3. Model: Business models like Todo which we will reference in business logic use cases and our UI

The DATA layer:

  1. Repository: Repository implementations
  2. 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)

Files and Folder Stucture

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

)

Enter fullscreen mode Exit fullscreen mode
data class TodoAPIEntity(
    val id: Int,
    val completed: Boolean,
    val title: String
)

fun TodoAPIEntity.toTodo(): Todo {
    return Todo(
        id = id,
        isCompleted = completed,
        task = title
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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() }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>

}
Enter fullscreen mode Exit fullscreen mode
class TodoRepositoryImpl(private val datasource: TodoDataSource) : TodoRepository {

    override suspend fun getTodos(): List<Todo> {
        return datasource.getTodos()
    }
}

Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
@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()
                    }
                }
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val vm = TodoViewModel(
            getTodosUseCase = GetTodos(
                repository = TodoRepositoryImpl(
                    api = TodoAPIImpl()
                )
            )
        )
        super.onCreate(savedInstanceState)
        setContent {
            CleantodoTheme {
                TodoListView(vm)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

so to recap:

Application Flow

List View Screen

Top comments (0)