Managing states with Kaskade
States are important in every application — it tells us what the app’s current landscape is and the user’s next actions. However, sometimes, state can not be immediately evident when it is managed in different places, especially with Android’s lifecycle wherein the screen can stop and resume in a lot of ways.
That’s why unidirectional data flows work. It makes states predictable by enforcing rules on when to do an action and when to update the application state; this results in more effective control of the application. This also makes testing easier by treating the flow as a black box that execute actions and observe states.
Yet Another State Container?
With lots of open source solutions and libraries implementing this kind of data flow — Why create another one?
- Lightweight — Unlike most implementations of MVI that uses RxJava extensively, I wanted a solution that enforces unidirectional data flow without the use of external dependencies.
- Modular — can be easily substituted to different implementation with or without the use of another library.
- Extendable — in relation to modular and lightweight, it’s important to extend the API and create user defined implementation to fit specific requirements.
- DSL (Domain Specific Language)— able to hide complexity in a fluent way.
These are the ideas that I keep in mind when developing Kaskade.

Giphy App
To demonstrate how Kaskade works, let’s create an app that gets the trending GIFs from Giphy.
Our first screen would be a list of GIFs we get from Giphy.



And a second screen where we view the GIF in full detail and get a random GIF.

For the sake of this article, we will only cover the List Screen.
Actions
An action contains the necessary inputs to represent anything that requires interaction from the user.
With the list screen we can come up with these actions:
- Refresh —from the SwipeRefreshLayout.
- Load More — from reaching the end of the list.
- Item Clicks — when the user clicks on an item.
- Error Action — when the application encounters an error.
Even though the error action is not visible here, it’s a good idea to always keep in mind that there will be an error state in your application.
We represent these actions in Kaskade as:
States
A state is a representation of your user interface. It contains information of how the screen should look like.
For the states of the list screen:
- Screen — a representation of what the screen looks like.
- Transitioning to the detail screen — a state where we are signaling the application to move onto the the detail screen.
- Error — indicates that an error has occurred
These states are represented as:
The Screen
state has four loading Modes
and a list of GiphyItem
which represents the list shown by the RecyclerView
.
REFRESH
—SwipeRefreshLayout
is refreshingLOAD_MORE
— reached the end of the list and start loading the next page ofgiphies
.IDLE_REFRESH
— when the list has finished refreshing, repopulate the list with new list ofgiphies
.IDLE_LOAD_MORE
—when loading GIFs from server is finished then addgiphies
at the end of the list.
Another thing to note is both Error
and GoToDetail
states are typed as SingleEvent
or SingleLiveEvent — these types are states that should only happen ONCE and cannot be considered as previous state in the Reducer.
Creating the Kaskade
With Kaskade DSL, there’s an easy to use syntax to create the flow of the application.
The DSL exposes a create()
function that accepts an initial state and is emitted as soon as the states are observed (or onStateChanged
function is instantiated with a non-null value). In Android, initial state is important in handling Process Death in order to pass state that is saved in onSavedInstanceState()
.
The on
method is the Reducer which takes a reified type of ListAction
and a lambda with ActionState
as a receiver.
The concept of reducer in Kaskade is different from the traditional reducer wherein the reducer’s responsibility is a general function to reduce actions and states into a new state while Kaskade pairs up an action and a reducer to create the new state. This way we have the freedom to add side effects in the reducer to come up with a new state for this action and the current state.
Kaskade also has an rx
builder DSL to create reducers that output an Observable
of ListState
.
Putting it together and using RxJava to create asynchronous reducers, we will have something like this:
In the example, the Kaskade
is created using the rx
builder to have a shared observer for actions inside the block, that is, both Refresh
and LoadMore
actions uses this observer
.
OnItemClick
and OnError
is not part of the rx
block and are not using the observer. They use the default reducer and run synchronously.
The DSL makes it easier to compose actions behaviors. You are able to create actions with asynchronous reducers using RxJava or Coroutines while also having reducers that runs synchronously.
Implementing ListAction.Refresh
and ListAction.LoadMore
, we have this following code:
The rx
builder’s on
method has a lambda parameter that has a receiver of Observable<ActionState>
; thus, we can immediately call flatMap
and pass in an observable.
In this case, loadTrending()
creates an Observable<ListState>
that starts with a loading state and finishes with an idle mode screen with the list that it gets from the Repository
.
Executing Actions
Now that we have created a Kaskade, we can start sending actions for it to process and emit new states.
Kaskade has a method to process actions:
But we would want to observe actions instead of calling the method every time. We can then wrap this method into an Observable.
Notice that it returns a Disposable
. The actions that it listens to usually will have a reference to the UI or View. Thus, to avoid memory leaks this needs to be disposed in onDestroyView
.
We create the actions observable using RxBinding:
Observing states
We have already created the Kaskade and passed actions to it, but nothing happens unless we observe the states emitted.
To observe the states, Kaskade has an extension:
This creates a LiveData
that emits the states from Kaskade. The DamLiveData
saves every latest emission by type except for SingleEvent
. Moreover, since it’s a LiveData, we can easily use it in Activities or Fragments without thinking about disposing the observer when the view is destroyed.
We can call a render function inside the LiveData observer to update the UI every time a state is emitted. The render function can be something like this:
Testing
With unidirectional flows testing is easier.
- Given a Kaskade
- Observe the State
- Process an Action
- Verify changes on the observer
With having state changes and actions more predictable, the tests can be simplified in four steps. This also means by adding features, we only need to add an action to handle and a state to show.
The tests for the sample app can be found here.
Putting it all together
Our final data flow will look something like this:

ViewModel abstracts the use of Kaskade as outside of it, we only have Action
and State
. Observing states uses LiveData
and actions are observed using Rx Observables. This makes it modular and Kaskade can be removed without affecting too much of the code.
Now we have a beautiful app that shows us trending GIFs in Giphy. Yay!

With Kaskade, we can have the benefits of a unidirectional data flow architecture that does not force itself into the architecture!
The code for the sample app is available here:
To know more you can visit the wiki.
Big thanks to Jasmine Villadarez for editing the article!