Domain Layer : Android app architecture notes

Jordan Mungujakisa
3 min readDec 3, 2023

--

Photo by Aaron Santelices on Unsplash

It is an optional layer between the UI layer and the data layer. It encapsulates complex business logic like data that is shared by multiple ViewModels. The classes in this layer are called use cases or interactors. This layer is optional and not all apps will need it, only use it to handle complexity and favor reusability.

Benefits of the domain layer

  • improves code readability
  • Improves testability
  • splits code responsibility resulting into smaller classes
  • Avoids code repetition

Each domain layer class should only have a single responsibility and should not hold mutable data. Mutable data should only be handled in the Data and UI layers.

UseCases

UseCases usually sit between the ViewModels and the Repositories and they communicate with the ViewModels using either Callbacks for java and Coroutines for Kotlin.

Since UseCases contain reusable logic, we can find instances where UseCases make use of other UseCases for example where multiple classes in the UI layer depend on timezones to display the correct message to the UI.

class GetLatestNewsWithAuthor(
private val newRepository: NewsRepository,
private val authorRepository: AuthorRepository,
private val formateDateUseCase: FormatDateUseCase
){
// combine the data sources depending on the date format
}
domain layer

UseCase class instances can be made callable as functions using the invoke() function and the operator modifier.

//we can later call instances of this class like a function
class FormatDateUseCase(val userRepository: UserRepository){
private val formatter = SimpleDateFormat(
userRepository.getPreferedDateFormat(),
userRepository.getPreferedLocale()
)

operator fun invoke(date: Date): String{
return formatter.format(date)
}
}
//In a ViewModel we can call the above UseCase as below;
val date = Calendar.getInstance()
val todaysDate = formatDateUseCase(date)

The invoke() method can take any number of parameters and return any type

UseCases do not have their own lifecycles but are scoped to the lifecycle of the classes that are calling them for example you can call them from classes in the UI layer, from Services, etc.

UseCases must be thread safe, if they are going to perform long running operations on the main thread then it is their responsibility to differ the operation to the appropriate thread.

class SomeUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatcher.Main
){
operator fun invoke() = withContext(defaultDispatcher){
//perform long running operation in the background thread
}
}

Common Domain Layer tasks

Reuse simple business logic

It is advisable to include the repeatable business logic found in the UI layer in the UseCases. Which makes it easy to make changes to the business logic and also test them in isolation.

It is discouraged put shared logic in Util classes because they are often difficult to find and their functionality hard to discover.

Combining UseCases

In some scenarios we may find ourselves that we need data from two different repositories for example where we need to display news articles and the details of there authors and we have these two information coming from two different repositories. Instead of bundling the logic in the UI Layer or ViewModel we can create another UseCase to perform this task, this encourages reusability and makes testing easier.

class GetNewsWithAuthoruseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
){
operator fun invoke(): List<ArticleWithAuthor>{
val news = newsRepository.fetchLatestNews()
val result: MutableList<ArticleWithAuthor> = mutableListOf()
for (article in news){
val author = authorsRepository.getAuthor(article.authorId)
result.add(ArticleWithAuthor(article, author))
}
return result
}
//this operation can be long running and we can consider differing it to a
//background thread as explained earlier
}
Restricting direct access between the UI Layer and the Data Layer? It is Optional, depending on your use case.

--

--

Jordan Mungujakisa

Mobile app alchemist who is trying to transmute elegant designs, into elegant code, into beautiful mobile app experiences.