Skip to content

Handling async jobs

Actor

If you have

  • anything asynchronous
  • more complex requirements how some Wish will modify the State

then now we need to distinguish between

  • incoming Wish
  • an actual Effect that is applied over the State using the Reducer

And now we need a mapping between the two. The Actor is basically a function doing just that:

typealias Actor<State, Wish, Effect> = (State, Wish) -> Observable<out Effect>

This means that now we can consider an incoming Wish and our current State, and based on them we can do some operations that will emit Effects to change our State.

Note

The operations do not have to be asynchronous. You can still use Observable.just() to return one or more Effects immediately. The added power here is that you can do that conditionally based on the current State

E.g. your Feature represents a form, and then based on the result of form validation over the current state, you can emit different Effects to signal validation success or error.

Important

Since invocations of the reducer must always happen on the same thread, you must ensure that you observe results of your asynchronous jobs on that thread. In Android, this practically means calling .observeOn(AndroidSchedulers.mainThread())

Excercise #2

Task

  • Let's talk to an async service to load some data
  • Let's signal whether we are in progress of loading, successfully loaded, or if an error has happened

Solution using ActorReducerFeature

class Feature2 : ActorReducerFeature<Wish, Effect, State, Nothing>(
    initialState = State(),
    actor = ActorImpl(),
    reducer = ReducerImpl()
) {

    data class State(
        val isLoading: Boolean = false,
        val payload: String? = null
    )

    sealed class Wish {
        object LoadNewData : Wish()
    }

    sealed class Effect {
        object StartedLoading : Effect()
        data class FinishedWithSuccess(val payload : String) : Effect()
        data class FinishedWithError(val throwable: Throwable) : Effect()
    }

    class ActorImpl : Actor<State, Wish, Effect> {
        private val service: Observable<String> = TODO()

        override fun invoke(state: State, wish: Wish): Observable<Effect> = when (wish) {
            is LoadNewData -> {
                if (!state.isLoading) {
                    service
                        .observeOn(AndroidSchedulers.mainThread())
                        .map { FinishedWithSuccess(payload = it) as Effect }
                        .startWith(StartedLoading)
                        .onErrorReturn { FinishedWithError(it) }
                }
                else {
                    Observable.empty()
                }
            }
        }
    }

    class ReducerImpl : Reducer<State, Effect> {
        override fun invoke(state: State, effect: Effect): State = when (effect) {
            is StartedLoading -> state.copy(
                isLoading = true
            )
            is FinishedWithSuccess -> state.copy(
                isLoading = false,
                payload = effect.payload
            )
            is FinishedWithError -> state.copy(
                isLoading = false
            )
        }
    }
}

Under the hood, ActorReducerFeature is a subclass of BaseFeature giving you a subset of all the possibilities there.

It will also wire everything up for you (reacting to a Wish, calling your Actor and subscribing to the Observable<Effect> returned by it, and calling your Reducer to emit your next State).

Note

In this example, the error result is not stored in the state. The preferred way in most cases is an event-based approach seen in the chapter News and inter-feature communication

But if you need it, you can still add a field in the State to store the error, just don't forget to reset it in the Reducer upon the next StartedLoading or FinishedWithSuccess effects.

Another approach would be to use a Kotlin sealed class, or the functional Either<A, B> type for the payload, where A would be the error, B would be actual data. Really only up to you.

When should you use ActorReducerFeature

  • There are async jobs in your Feature
  • There's some extra business logic involving how to react to a Wish conditionally