Introduction
- In this series we are going to be talking about code refactoring, which is considered a more intermediate topic. So if you never have heard about UseCases or the Android clean code architecture, then this tutorial series might not be for you.
- Also, I would like to point out that these patterns are really more like guide lines and not strict rules that you must abide by.
Resources
- I found these refactoring patterns in the Greenstand Android GitHub repository. Specifically, I looked at these 3 files:
3) Multiple parameters UseCase
1) UsesCases Abstraction
- So the UseCase abstraction looks like this:
abstract class UseCase<in Params : Any, out Result> {
abstract suspend fun execute(params: Params): Result
}
- Just a little reminder, all Abstract classes in Kotlin are open by default and can not be instantiated but only inherited. We use this abstract class to implement the same behaviour across related UseCase objects, which allows us to establish a
is a
relationship. - The next thing I should point out is the
in
andout
operators. Which are calledcontravariant
andvariance
annotations. Without diving too deep into generics, we can understand that thein
(contravariant) operator means it can only be consumed and never produced(not allowed as a return type). Theout
(variance) operator means it is to be only produced(used as a return type)
2) No parameters UseCase
- So if we start with a UseCase code like:
class LogoutUseCase constructor(
private val authRepository: AuthRepository
) {
suspend fun invoke():Boolean{
return authRepository.signUserOut()
}
}
- Then we can implement our Abstract UseCase, resulting in:
class LogoutUseCase constructor(
private val authRepository: AuthRepository
):UseCase<Unit,Boolean>() {
override suspend fun execute(params: Unit): Boolean {
return authRepository.signUserOut()
}
}
- So lets talk about the
Unit
class being used inexecute(params: Unit)
.Unit
type fulfills the same function as Java'svoid
. But what makes the two different is thatUnit
is a full fledged type and unlike void, it can be used as a type argument. Which makes it useful when combined with generics. To call theexecute()
method we would do so like this:
logoutUseCaseInstance.execute(Unit)
- We are able to do this with
Unit
because it is a singleton and returns the Unit object
3) Multiple parameters UseCase
- Now we are going to understand how to use this pattern when we are faced with multiple parameters. So starting with a class like:
class CreateUserUseCase constructor(
private val database:DatabaseRepository
){
suspend operator fun invoke(email:String, username:String):Flow<Response<Actions>>{
return database.createUser(email, username)
}
}
- Then when we implement our pattern, we get:
data class CreateUserParams(
val email: String,
val username: String,
)
class CreateUserUseCase constructor(
private val database:DatabaseRepository
):UseCase<CreateUserParams,Flow<Response<Actions>>>(){
override suspend fun execute(params: CreateUserParams): Flow<Response<Actions>> {
return database.createUser(params.email, params.username)
}
}
Notice the
CreateUserParams
class, this is how we are dealing with the problem of multiple parameters. By creating a data class we are able to handle any number of parameters which are needed.While this might seem like extra work, what this refactoring really does is makes our code more readable and predictable.
Conclusion
- Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
Top comments (1)
This refactoring actually makes our code more understandable and predictable, despite the fact that it may appear to be extra work.