Clean Architecture Domain layer modeling with Kotlin, RxJava and Arrow.

Website

The project it’s all about modeling domain use cases and domain error system with kotlin and RxJava.

It’s intended for those how wre already familiar with Clean Architecture approach.

To familiarize yourself with the concept I recommend starting with these great posts.

  1. Android-CleanArchitecture
  2. applying-clean-architecture-on-android-hands-on
Lets recap the basic concepts of Clean Architecture
The benefits of pure java domain layer:

The project demonstrate simple domain layer with 4 use cases

Now, let’s dive in to Use Cases structure

Modeling Use Cases With RxJava

Utilizing Reactive types, we’ll demonstrate a Use Case Object, with and without a parameter.

How to create use case

Use case composed from

  1. Reactive type - (Observable/Flowable/Single/Maybe/Completable)
  2. Data (Optional) - The data which use case will emit
  3. Error (Optional) - Expected use case error and will be sealed class
  4. Parameter (Optional)

Basic use case structure

With parameter

T - reactive type

P - parameter type

interface UseCaseWithParam<out T, in P> {

    fun build(param: P): T

    fun execute(param: P): T
}
Without parameter
interface UseCaseWithoutParam<out T> {

    fun build(): T

    fun execute(): T
}

Use case examples

Login use case

Reactive type: Maybe

:white_check_mark: Parameter

:white_check_mark: Error

:x: Data

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()
    }

}

Get posts use case

Use case type:

Reactive type: Observable

:x: Parameter

:white_check_mark: Error

:white_check_mark: Data

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 domain error system with Kotlin and Arrow Either

The improvements to the regular rx error system

  1. Separation between expected and unexpected errors
  2. Pattern matching for error state with kotlin sealed classes.
  3. 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.

Creating either stream

You can create Either stream in one of the following ways

  1. Defining Either observable
    class CreateEither {
     fun create() {
       Observable.just<Either<Exception, String>>(Success("Hello"))
                 .subscribe()
     }
    }        
    
  2. 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()
  
    }
}

Operating on either stream

Observable.just<Either<Exception, String>>(Success("Hello"))
            .filter({ it.isRight() })
            .map { Failure(Exception()) }
            .fold({"on failure"},{"on success"})

Consuming either stream

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()
            }
        )
    })

Example of consuming use case and handling expect and unexpected errors separately

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()
}