This project is about what kotlin and RxJava can give us in context of modeling domain use cases and error system in clean architecture approach.
It's intended for those already familiar with Clean Architecture approach. To familiarize yourself with the concept I recommend starting with these great posts.
- Keeping the code clean with single responsibly principle.
- Isolation between layers: domain, data and presentation.
- Using the principle of Inversion of Control to make the domain independent from frameworks.
- Easy change frameworks "Plug and Play"
- Easy share code between platform
- Fester tests
Utilizing Reactive types, we'll demonstrate a Use Case Object, with and without a parameter.
Use case composed from
- Reactive type - (Observable/Flowable/Single/Maybe/Completable)
- Data (Optional) - The data which use case will emit
- Error (Optional) - Expected use case error and will be sealed class
- Parameter (Optional)
T - reactive type
P - parameter type
interface UseCaseWithParam<out T, in P> {
fun build(param: P): T
fun execute(param: P): T
}
interface UseCaseWithoutParam<out T> {
fun build(): T
fun execute(): T
}
class LoginUseCase(
private val authenticationService: AuthenticationService,
private val validationService: ValidationService,
private val userService: UserService,
threadExecutor: Scheduler,
postExecutionThread: Scheduler
) : MaybeWithParamUseCase<LoginUseCase.Error, LoginUseCase.Param>(
threadExecutor,
postExecutionThread
) {
override fun build(param: Param)
: Maybe<Error> {
//implementation
}
data class Param(val email: String, val password: String)
sealed class Error {
object InvalidEmail : Error()
object InvalidPassword : Error()
object EmailNotExist : Error()
object WrongPassword : Error()
object NoNetwork : Error()
}
}
class GetPostsUseCase(
private val postRepository: PostRepository,
private val userService: UserService,
threadExecutor: Scheduler,
postExecutionThread: Scheduler
) : ObservableWithoutParamUseCase<Either<GetPostsUseCase.Error, GetPostsUseCase.Data>>(
threadExecutor,
postExecutionThread
) {
override fun build()
: Observable<Either<Error, Data>> {
//implementation
}
sealed class Error {
object NoNetwork : Error()
object UserNotLogin : Error()
object PostNotFound : Error()
}
data class Data(val id: String, var text: String)
}
Modeling error system with Kotlin and Arrow Either
- Separation between expected and unexpected errors
- Pattern matching for error state with kotlin sealed classes.
- Keep the stream alive in case of expected errors, stop the stream only on unexpected or fatal errors.
The implementation is with either stream Observable<Either<Error, Data>>
and since error is sealed class we can do pattern matching on it.
The regular rx on error used for unexpected errors only.
You can create Either stream in one of the following ways
- Defining Either observable
class CreateEither {
fun create() {
Observable.just<Either<Exception, String>>(Success("Hello"))
.subscribe()
}
}
- Converting regular stream to either stream with toSuccess/toFailure
private fun <T> Observable<T>.toSuccess() = map { Success(it) }
private fun <T> Observable<T>.toFailure() = map { Failure(it) }
class CreateEither {
fun toEither() {
Observable.just("Hello Either")
.toSuccess()
Observable.just("Hello Either")
.toFailure()
}
}
- Fold - applies
success
block if this is a Success orfailure
if this is a Failure.
Observable.just<Either<Exception, String>>(Success("Hello"))
.filter({ it.isRight() })
.map { Failure(Exception()) }
.fold({"on failure"},{"on success"})
someUseCase
.execute(SomeUseCase.Param("Hello World!"))
.subscribe(object : ObservableEitherObserver<SomeUseCase.Error, SomeUseCase.Data> {
override fun onSubscribe(d: Disposable) = TODO()
override fun onComplete() = TODO()
override fun onError(e: Throwable) = onUnexpectedError(e)
override fun onNextSuccess(r: SomeUseCase.Data) = showData(r)
override fun onNextFailure(l: SomeUseCase.Error) = onFailure(
when (l) {
SomeUseCase.Error.ErrorA -> TODO()
SomeUseCase.Error.ErrorB -> TODO()
}
)
})
class SomePresenter(val someUseCase: SomeUseCase) {
fun some() {
someUseCase
.execute(SomeUseCase.Param("Hello World!"))
.subscribe(object : ObservableEitherObserver<SomeUseCase.Error, SomeUseCase.Data> {
override fun onSubscribe(d: Disposable) = TODO()
override fun onComplete() = TODO()
override fun onError(e: Throwable) = onUnexpectedError(e)
override fun onNextSuccess(r: SomeUseCase.Data) = showData(r)
override fun onNextFailure(l: SomeUseCase.Error) = onFailure(
when (l) {
SomeUseCase.Error.ErrorA -> TODO()
SomeUseCase.Error.ErrorB -> TODO()
}
)
})
}
private fun onFailure(any: Any): Nothing = TODO()
private fun showData(data: SomeUseCase.Data): Nothing = TODO()
private fun onUnexpectedError(e: Throwable): Nothing = TODO()
}