Skip to content

4. States

James Shvarts edited this page Dec 11, 2018 · 1 revision

A State is a central concept of this architecture. It represents what's displayed on the screen (or a part of the screen if the latter is composed of multiple ViewModels).

We can model States as either data classes or sealed classes. The former allows us to take advantage of the .copy() function while creating a new State based on a previous one. However, when your State is very complex, it may be easier to model and maintain it as a sealed class.

State as a data class

The note detail screen could contain the following State:

    data class State(val note: Note? = null,
                 val isIdle: Boolean = false,
                 val isLoading: Boolean = false,
                 val isLoadError: Boolean = false,
                 val isNoteDeleted: Boolean = false,
                 val isDeleteError: Boolean = false) : BaseState

State as a sealed class

The note detail screen could contain the following State:

    sealed class State {
        data class NoteDetail(val note: Note) : State()
        object Idle : State()
        object Loading : State()
        object LoadError : State()
        object Deleted : State()
        object DeleteError : State()
    }

To handle Process Death, you'd need to make the States Parcelable. Refer to the Handling Process Death section.

Initial State

ViewModels in Roxie are designed to accept an initialState in the constructor. If none is passed in, we are required to set it when the ViewModel is first initialized:

    override val initialState = initialState ?: State(isIdle = true)

The Idle State is a special State necessary to "seed" the Reducer. This State is usually not emitted to the UI since it has no practical use to the user.

Transforming Changes into States

We can transform Changes into States using a chain similar to this one:

    disposables += allChanges
        .scan(initialState, reducer)
        .filter { !it.isIdle }
        .distinctUntilChanged()
        .doOnNext { Timber.d("Received state: $it") }
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(state::setValue, Timber::e)

where the scan() operator transforms new Changes into the new States. Note how we filter out the Idle state that the UI is not interested in. Note that the distinctUntilChanged() operator prevents emitting the duplicate States one after another. We log the States emitted in doOnNext() to make debugging easier. See the section on Logging for more info.

Once again, the block above showcases the power of RxJava. Quite a bit of work is done in a concise composable way.

setValue vs postValue

Note that we use state::setValue above. This is because this particular ViewModel handles Actions designed to produce multiple States. We could not use state::postValue in this case since the latter could potentially "lose" some of the intermediate States. The official documentation contains:

If you called this method multiple times before the main thread executed a posted task, only the last value would be dispatched.

If we were only emitting a single State from a ViewModel, we could replace:

    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(state::setValue, Timber::e)

with a single line:

    .subscribe(state::postValue, Timber::e)

Sharing States

Multiple Fragments could observe the same State. Just use the same ViewModel with the host Activity as the LifecycleOwner. See an example here