Clean Architecture: Android App
Photo by Pixabay from Pexels

Clean Architecture: Android App

By employing clean architecture, you can design applications with very low coupling and independent of technical implementation details. That way, the application becomes easy to maintain and flexible to change. Clean architecture allows us to create architectural boundaries between dependencies which allows components to be intrinsically testable.

What's to follow is our attempt to build an Android application using clean architecture with TDD. The app will be used to manage and organize contacts.

We are going to structure our application in the following way:

Our application is partitioned into Presentation, Domain and Data Layers.

The Presentation layer is responsible for all consumer facing components like views and view models.

The Domain Layer holds all business Logic (use cases) and System Logic (repositories).

And the Data Layer holds all infrastructure components, like data sources and services.

At specific points in our applications we define dependency rules with interfaces. The architecture of the system is defined by these boundaries that separate components and shows their dependencies. We unit-test every component by mocking its dependencies.

Note: We don't unit-test our views as they should contain minimal logic to aid in rendering data graphically.

System Structure

We structure the application in the following way to show intent by file / folder structure

│── presentation
│   └── contact
│       ├── create
│       │   ├── CreateContactViewModel.kt
│       │   └── CreateContactView.kt
│       ├── edit
│       │   ├── EditContactViewModel.kt
│       │   └── EditContactView.kt
│       └── list
│           ├── ListContactViewModel.kt
│           └── ListContactView.kt
├── domain
│   ├── interfaces
│   │   ├── usecases
│   │   │   └── contact
│   │   │       ├── CreateContactUseCase.kt
│   │   │       ├── UpdateContactUseCase.kt
│   │   │       ├── DeleteContactUseCase.kt
│   │   │       ├── GetContactUseCase.kt
│   │   │       └── GetAllContactsUseCase.kt
│   │   └── repositories
│   │       └── ContactRepository.kt
│   ├── models
│   │   └── Contact.kt
│   ├── usecases
│   │   └── contact
│   │       ├── CreateContact.kt
│   │       ├── UpdateContact.kt
│   │       ├── DeleteContact.kt
│   │       ├── GetAllContacts.kt
│   │       └── GetContact.kt
│   └── repositories
│       └── ContactRepositoryImpl.kt
└── data
    ├── interfaces
    │   ├── ContactDataSource.kt
    │   └── ContactDao.kt
    └── datasources
        └── room
          ├── entities
          │   └── ContactRoomEntity.kt
          └── RoomContactDataSource.kt

Let's show an example of a single vertical slice of the application. This slice would be to display a list of contacts on a screen.

Let's start with the view model.

1class ListContactsViewModel constructor( 2 private val getAllContactsUseCase: GetAllContactsUseCase 3) : 4 ViewModel() { 5 private val _errorMessage = mutableStateOf("") 6 private val _contacts = mutableStateListOf<ContactResponseModel>() 7 8 val errorMessage: String 9 get() = _errorMessage.value 10 11 12 val contacts: List<ContactResponseModel> 13 get() = _contacts.toList() 14 15 suspend fun getContacts() { 16 try { 17 _contacts.clear() 18 val list = getAllContactsUseCase.execute() 19 _contacts.addAll(list) 20 } catch (err: Exception) { 21 _errorMessage.value = "Error Fetching Contacts" 22 false 23 } 24 } 25}

View Model

This view model has 2 public facing properties:

  1. The list of contacts (line 12),
  2. The loading contacts function (line 15), when called, internally updates the state of the view model after data is retrieved.

Use Case

We see the view model has one dependency, the GetAllContactsUseCase, which is constructor injected. Which means we can TDD develop this component by mocking the use case dependency.

All use case classes have one function, the execute function, and is enforced by the following interface:

1interface GetAllContactsUseCase { 2 suspend fun execute(): List<ContactResponseModel> 3}

Data Models

When data travels between different parts of the system, they move to and from the data source in opposite directions. We need to define the shape of that data. In most cases, we need a request model and response model. These models we can define in the contact model file

1package za.co.nanosoft.cleancontacts.domain 2 3data class ContactResponseModel( 4 val id: Int, 5 val name: String 6) 7 8 9data class ContactRequestModel( 10 val id: Int? = null, 11 val name: String 12)

These models will be used throughout the system and will be the conduit for up and down data.

We can further define architectural boundary interfaces for contact repository and contact data source:

ContactRepository

1interface ContactRepository { 2 suspend fun getContacts(): List<ContactResponseModel> 3 suspend fun getContact(id: String): ContactResponseModel 4 suspend fun deleteContact(id: String): Boolean 5 suspend fun updateContact(id: String, data: ContactRequestModel): Boolean 6 suspend fun createContact(data: ContactRequestModel): Boolean 7}

ContactDataSource

1interface ContactDataSource { 2 suspend fun getAll(): List<ContactResponseModel> 3 suspend fun getOne(id: String): ContactResponseModel 4 suspend fun delete(id: String): Boolean 5 suspend fun update(id: String, data: ContactRequestModel): Boolean 6 suspend fun create(data: ContactRequestModel): Boolean 7}

You get the picture. To follow further, checkout the GitHub which includes tests for all testable components.

Check out the github repo here