-
Notifications
You must be signed in to change notification settings - Fork 1
Use cases
To perform some actions with the application domain, we need to implement use cases. This documentation will implement the SendName
use case, which stores the name in a database and returns it.
Every use case of the application has to be an indirect implementation of the UseCase
interface. It needs to be passed the request and response types, which should implement the UseCase.UseCaseRequest
and UseCase.UseCaseResponse
interfaces, respectively. The only method within the interface is execute()
, which executes the use case given the request and generates a UseCaseResult
with the response.
The UseCaseResult
is a sealed class that contains the possible results of the execution of the use case:
-
Success
: the use case is executed successfully and has the result as its property. -
Error
: there has been an error while executing the use case and has the exception as its property.
Next, we will define the SendName
use case. We will use an interface to decouple the use cases from the ViewModel
. The interface has to extend from UseCase
and define the request and response of the use case. The request and response will be data classes that implements the UseCase.UseCaseRequest
and UseCase.UseCaseResponse
interfaces, respectively. The names of those classes will be SendName.Request
and SendName.Response
.
interface SendName : UseCase<SendName.Request, SendName.Response> {
data class Request(
val name: String
) : UseCase.UseCaseRequest
data class Response(
val name: String
) : UseCase.UseCaseResponse
}
Next, we have to implement the SendName
interface. The class will be called SendNameImpl
and implement the execute()
method from the UseCase
interface.
The communication between the use case and the data has to be done using the repository pattern. We have to define a repository class that will hide from the use case where the data is stored or provided. This communication has to be done using interfaces to decouple the layers, which will be passed using dependency injection.
class SendNameImpl @Inject constructor(
private val nameRepository: NameRepository
) : SendName {
override suspend fun execute(request: SendName.Request): UseCaseResult<SendName.Response> {
// ...
}
}
The execute()
method takes as a parameter a SendName.Request
instance and returns a UseCaseResult<SendName.Response>
object. In this case, the use case will only make a call to the repository with a name, which will return from it. If an exception is thrown while executing the use case, then the answer will be of type UseCaseResult.Error
, and the exception will be stored as a parameter of it. Otherwise, the answer will be of type UseCaseResult.Success
, and the result of the use case will be stored as a parameter. This suggests using a try-catch block, which will return the result depending on if an exception is thrown.
override suspend fun execute(request: SendName.Request): UseCaseResult<SendName.Response> {
return try {
val name = nameRepository.storeName(request.name)
UseCaseResult.Success(SendName.Response(name))
} catch (e: Exception) {
UseCaseResult.Error(e)
}
}
We can refactor the previous method to make it more Kotlin friendly.
override suspend fun execute(request: SendName.Request) = try {
val name = nameRepository.storeName(request.name)
UseCaseResult.Success(SendName.Response(name))
} catch (e: Exception) {
UseCaseResult.Error(e)
}
The structure of all the implementations of use cases is similar, since all of them will be a try catch block as shown in the previous section. In order to improve readability there is a method called runUseCase()
which will execute any use case and return the result as expected, depending on if it is a Success
or an Error
.
override suspend fun execute(request: SendName.Request) = runUseCase {
val name = nameRepository.storeName(request.name)
SendName.Response(name)
}
Table of contents
Updates