Skip to content

Blueprint Interactor Coroutines

This library provides building blocks for writing Interactors (use cases as defined in Clean Architecture) based on Kotlin Coroutines and Flow.

Dependency

implementation "io.github.reactivecircus.blueprint:blueprint-interactor-coroutines:${blueprint_version}"

Usage

To implement an Interactor we would extend from one of the 2 classes provided:

  • SuspendingInteractor for single-shot tasks
  • FlowInteractor for cold streams

Let’s say we need an Interactor for fetching a list of users from API. Since there can only be 1 response (or error), our Interactor should extend from SuspendingInteractor:

class FetchUsers(
    private val userService: UserService,
    coroutineDispatcherProvider: CoroutineDispatcherProvider
) : SuspendingInteractor<EmptyParams, List<User>>() {
    override val dispatcher: CoroutineDispatcher = coroutineDispatcherProvider.io

    override suspend fun doWork(params: EmptyParams): List<User> {
        userService.fetchUsers()
    }
}

To execute this Interactor, call the execute(...) suspend function from a CoroutineScope:

viewModelScope.launch {
    // EmptyParams is a special [InteractorParams] which you can use when the Interactor has no params.
    val users: List<User> = fetchUsers.execute(EmptyParams)
}

Note that the CoroutineDispatcherProvider in the constructor of the Interactor comes from the blueprint-async-coroutines artifact, which encapsulates the threading behavior with a wrapper API.

Now let’s implement another Interactor for updating a user profile. This interactor expects no result and we just need to know the whether it has been completed successfully. So our Interactor should again extend from SuspendingInteractor:

class UpdateUserProfile(
    private val userService: UserService,
    coroutineDispatcherProvider: CoroutineDispatcherProvider
) : SuspendingInteractor<UpdateUserProfile.Params, Unit>() {
    override val dispatcher: CoroutineDispatcher = coroutineDispatcherProvider.io

    override suspend fun doWork(params: Params) {
        userService.updateUserProfile(params.userProfile)
    }

    class Params(internal val userProfile: UserProfile) : InteractorParams
}

To execute this Interactor, call the execute(...) suspend function from a CoroutineScope:

viewModelScope.launch {
    // this returns a Unit
    updateUserProfile.execute(UpdateUserProfile.Params(userProfile))
}

In a reactive architecture we might want to stream any changes to the users persisted in the database to automatically re-render the UI whenever the user list has changed. In this case it makes sense to extend from the FlowInteractor:

class StreamUsers(
    private val userRepository: UserRepository,
    coroutineDispatcherProvider: CoroutineDispatcherProvider
) : FlowInteractor<EmptyParams, List<User>>() {
    override val dispatcher: CoroutineDispatcher = coroutineDispatcherProvider.io

    override fun createFlow(params: Params): Flow<List<User>> {
        return userRepository.streamUsers() // this returns a Coroutines Flow
            .map { users ->
                users.sortedBy { it.lastName }
            }
    }
}

On the call-side:

streamUsers.buildFlow(EmptyParams)
    .onEach { users ->
        // propagate value of each Flow emission to LiveData<List<User>>
        usersLiveData.value = users
    }
    .catch {
        Timber.e(it)
    }
    // launch the collection of the Flow in the [viewModelScope] from "androidx.lifecycle:lifecycle-viewmodel-ktx"
    .launchIn(viewModelScope)

Please check the Blueprint Coroutines Demo app for more examples of writing and testing Interactors.