(Reactive Redux) Simply Managing State with RxJava on Android

Zeyad Gasser

So the hot new topic now in the android community is managing state. This post requires some RxJava experience. So, its quite simple actually, to manage state of any component of your app, from activities to views, it takes 3 components and 4 steps.

Merge All the Events

Lets say we have an activity that has a list of users, which you can search and delete from. So, here we have 3 events, including loading the data. A goto tool for that is a lib called RxBinding, which turns all the event listeners of UI widgets to observables. So construct all those events then merge them together. Every event should be mapped into an object that should also contain any data you need to execute.

Use a BaseEvent class as a parent all your events for type safety.

public abstract class BaseEvent {
}

Merging event streams.

public Observable<BaseEvent> events;Observable getUsersStream = Observable.just(new GetUsersEvent());Observable searchStream =RxSearchView.queryTextChanges(mSearchView)
.filter(charSequence -> !charSequence.toString().isEmpty())
.map(query -> new SearchUsersEvent(query.toString()))
.throttleLast(100, TimeUnit.MILLISECONDS)
.debounce(200, TimeUnit.MILLISECONDS)
.onBackpressureLatest();
Observable deleteStream = Observable.defer(() -> RxMenuItem.clicks(menu.findItem(R.id.delete_item))
.map(click -> new DeleteUsersEvent(usersAdapter.getSelectedItemsIds()))
.doOnEach(notification -> {
actionMode.finish();
Log.d("deleteEvent", "eventFired");
}));
events = Observable.merge(getUsersStream, searchStream, deleteStream);

Map Events to Executables

Now we want to map those events to service calls or any logic that we need. The getUsers event needs to be mapped to a service call that returns an observable of books and so on. So we need a mapping function that maps events to executables. Every executable should return the same generic object, for simplicity, lets call it Result. Result has 4fields, isLoading, isSuccessful, error, state, bundle.

Events to Executables Mapper:

Func1<Event, Observable<?>> mapEventsToExecutables = event -> {
Observable executable = Observable.empty();
if (event instanceof GetUsersEvent)
executable = userRepo.getUsers();
else if (event instanceof DeleteUsersEvent)
executable = userRepo.deleteCollection(((DeleteUsersEvent) event).getSelectedItemsIds());
else if (event instanceof SearchUsersEvent)
executable = userRepo.search(((SearchUsersEvent) event).getQuery());
return executable;
}

Result:

class Result<B> {

static final Result IN_FLIGHT = new Result<>(true, null, false, null);

private final boolean isLoading, isSuccessful;
private final Throwable error;
private final B bundle;

private Result(boolean isLoading, Throwable error, boolean isSuccessful, B bundle) {
this.isLoading = isLoading;
this.error = error;
this.isSuccessful = isSuccessful;
this.bundle = bundle; }

static <B> Result<B> errorResult(Throwable error) {
return new Result<>(false, error, false, null);
}

static <B> Result<B> successResult(B bundle) {
return new Result<>(false, null, true, bundle);
}
}

With the Result Object, we can describe which state are we in while our logic is running and a way to receive the output or errors safely. We need to map every successful outcome to a successful result, every error to an error Result and we start with a loading result.

.map(Result::successResult)
.onErrorReturn(Result::errorResult)
.startWith(Result.IN_FLIGHT)

State Accumulator

Now we need to convert every result to a state that we can then render on our activity. We start with an idle state then accumulate results to create new states. A great tool to achieve this is the scan operator, it can take an initial state an Func2 with the current state and the result as input and a final state as output.

State: (confusingly named UIModel)

class UIModel<B> {    public static final UIModel idleState = new UIModel<>(false, null, false, null);

private final boolean isLoading, isSuccessful;
private final Throwable error;
private final B bundle;

private UIModel(boolean isLoading, Throwable error, boolean isSuccessful, B bundle) {
this.isLoading = isLoading;
this.error = error;
this.isSuccessful = isSuccessful;
this.bundle = bundle;
}

public static <B> UIModel<B> loadingState(B bundle) {
return new UIModel<>(true, null, false, bundle);
}

public static <B> UIModel<B> idleState(B bundle) {
return new UIModel<>(false, null, false, bundle);
}

public static <B> UIModel<B> errorState(Throwable error) {
return new UIModel<>(false, error, false, null);
}

public static <B> UIModel<B> successState(B bundle) {
return new UIModel<>(false, null, true, bundle);
}

Scan operator

.scan(initialState, (currentUIModel, result) -> {
UserListState bundle = currentUIModel.getBundle();
if (result.isLoading())
currentUIModel = UIModel.loadingState(bundle);
else if (result.isSuccessful()) {
currentUIModel = UIModel.successState(UserListState.builder().setUsers(result.getBundle()).build());
} else currentUIModel = UIModel.errorState(result.getError());
return currentUIModel;
})

I find it a good practice have another object that represents everything about your success states, since they can be multiple. A builder pattern here makes things easy.

Note that Result and UIModel are immutable objects.

Putting it all together

A transformer that combines all these components and also runs all of it in the background and outputs to the UI Thread.

public Transformer<BaseEvent, UIModel<S>> uiModels(Func1<BaseEvent, Observable<?>> mapEventsToExecutables,
Func2<UIModel<S>, Result, UIModel<S>> stateAccumulator,
UIModel<S> initialState) {
return events -> events.observeOn(Schedulers.io())
.flatMap(event -> Observable.just(event)
.flatMap(mapEventsToExecutables)
.map(Result::successResult)
.onErrorReturn(Result::errorResult)
.startWith(Result.IN_FLIGHT))
.scan(initialState, stateAccumulator)
.observeOn(AndroidSchedulers.mainThread());
}

So in action it would look like this:

Func1<Event, Observable<?>> mapEventsToExecutables = event -> {
Observable executable = Observable.empty();
if (event instanceof GetUsersEvent)
executable = userRepo.getUsers();
else if (event instanceof DeleteUsersEvent)
executable = userRepo.deleteCollection(((DeleteUsersEvent) event).getSelectedItemsIds());
else if (event instanceof SearchUsersEvent)
executable = userRepo.search(((SearchUsersEvent) event).getQuery());
return executable;
}
Func2<UIModel<S>, Result, UIModel<S>> stateAccumulator =(currentUIModel, result) -> {
UserListState bundle = currentUIModel.getBundle();
if (result.isLoading())
currentUIModel = UIModel.loadingState(bundle);
else if (result.isSuccessful()) {
currentUIModel = UIModel.successState(UserListState.builder().setUsers(result.getBundle()).build());
} else currentUIModel = UIModel.errorState(result.getError());
return currentUIModel;
}
events.compose(uiModels(mapEventsToExecutables, stateAccumulator)
.subscribe({uiModel -> {
view.toggleViews(uiModel.isLoading());
if (!uiModel.isLoading()) {
if (uiModel.isSuccessful()) {
view.renderState(uiModel.getBundle());
} else if (uiModel.getError() != null) {
view.showError(throwable);
}
}
}, throwable -> new OnErrorNotImplementedException(throwable);}); // crash your app for unexpected errors

Thats it. With this setup we gain a very reliable state manager, which is has unidirectional data flow and offers a single source of truth, with reproducible outputs. Also you do not have to test anything other than your executables. You can find all this code here and examples here. Please like this post and share it thanks.

PS. Feel free to take a look at the whole project where i explore a generic implementing of the Clean Architecture by Uncle Bob, to save precious development time. A future blog post is coming soon :)

Zeyad Gasser

Written by

Senior Android Developer at Glovo. Twitter:@ZeyadG37

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade