(Reactive Redux) Simply Managing State with RxJava on Android, Part 2

Zeyad Gasser
Jun 7, 2017 · 4 min read

Remove duplicate Loading Events

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());
}
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))
.distinctUntilChanged((result1, result2) -> result1.isLoading() && result2.isLoading())
.scan(initialState, stateAccumulator)
.observeOn(AndroidSchedulers.mainThread());
}
distinctUntilChanged(new Func2<Result<?>, Result<?>>() {
@Override
public boolean test(Result<?> result1, Result<?> result2) throws Exception {
return result1.getBundle().equals(result2.getBundle()) ||
(result1.isLoading() && result2.isLoading());
}
}))

Add Events to the State Accumulator

class ResultBundle<E extends BaseEvent, B> {

private final String event;
private final B bundle;

ResultBundle(E event, B bundle) {
this.event = event.getClass().getSimpleName();
this.bundle = bundle;
}
//... getters
}
final class Result<B> {

private final boolean isLoading, isSuccessful;
private final Throwable error;
private final String state;
private final ResultBundle<?, B> bundle;

private Result(String state, boolean isLoading, Throwable error, boolean isSuccessful, ResultBundle<?, B> bundle) {
this.isLoading = isLoading;
this.error = error;
this.isSuccessful = isSuccessful;
this.bundle = bundle;
this.state = state;
}

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

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

static <B> Result<B> successResult(ResultBundle<?, B> bundle) {
return new Result<>(SUCCESS, false, null, true, bundle);
}
B getBundle() {
return bundle != null && bundle.getBundle() != null ? bundle.getBundle() : (B) new Object();
}
}
flatMap(event -> Observable.just(event)
.flatMap(mapEventsToExecutables())
.map(result -> Result.successResult(new ResultBundle<>(event, result)))
.onErrorReturn(Result::errorResult)
.startWith(Result.loadingResult()))
.scan(UIModel.idleState(initialState), (currentUIModel, result) -> {
S bundle = currentUIModel.getBundle();
if (result.isLoading()) {
currentUIModel = UIModel.loadingState(bundle);
} else if (result.isSuccessful()) {
if (result.getEvent().equals("GetUsersEvent"))
currentUIModel =
UIModel.successState(UserListState.builder().setUsers(result.getBundle()).build());
} else {
currentUIModel = UIModel.errorState(result.getError(), bundle);
}
return currentUIModel;
})
public interface SuccessStateAccumulator<S> {
S accumulateSuccessStates(Object newResult, String event, S currentStateBundle);
}
.scan(UIModel.idleState(initialState), (currentUIModel, result) -> {
S bundle = currentUIModel.getBundle();
if (result.isLoading()) {
currentUIModel = UIModel.loadingState(bundle);
} else if (result.isSuccessful()) {
currentUIModel = UIModel.successState(successStateAccumulator.accumulateSuccessStates(result.getBundle(), result.getEvent(), bundle));
} else {
currentUIModel = UIModel.errorState(result.getError(), bundle);
}
return currentUIModel;
})
public Transformer<BaseEvent, UIModel<S>> uiModels(
Func1<BaseEvent, Observable<?>> mapEventsToExecutables, SuccessStateAccumulator<S>, UIModel<S> initialState) {
return events -> events.observeOn(Schedulers.io())
.flatMap(event -> Flowable.just(event)
.flatMap(mapEventsToExecutables())
.map(result -> Result.successResult(new ResultBundle<>(event, result)))
.onErrorReturn(Result::errorResult)
.startWith(Result.loadingResult()))
.distinctUntilChanged((result1, result2) ->
result1.getBundle().equals(result2.getBundle()) ||
(result1.isLoading() && result2.isLoading()))
.scan(UIModel.idleState(initialState), (currentUIModel, result) -> {
S bundle = currentUIModel.getBundle();
if (result.isLoading()) {
currentUIModel = UIModel.loadingState(bundle);
} else if (result.isSuccessful()) {
currentUIModel =
UIModel.successState(successStateAccumulator.accumulateSuccessStates(result.getBundle(), result.getEvent(), bundle));
} else {
currentUIModel = UIModel.errorState(result.getError(), bundle);
}
return currentUIModel;
})
.observeOn(AndroidSchedulers.mainThread());
}
uiModelsTransformer = uiModels(
event -> { // event to executable mapper
Observable executable = Observable.empty();
if (event instanceof GetUsersEvent) {
executable = getUsers(((GetUsersEvent) event).getLastId());
} else if (event instanceof SearchUsersEvent) {
executable = search(((SearchUsersEvent) event).getQuery());
}
return executable;
},
(newResult, event, currentStateBundle) -> {//successStateAccumulator
List resultList = (List) newResult;
List<User> users = currentStateBundle == null ? new ArrayList<>() : currentStateBundle.getUsers();
List<User> searchList = new ArrayList<>();
switch (event) {
case "GetUsersEvent":
users.addAll(resultList);
break;
case "SearchUsersEvent":
searchList.clear();
searchList.addAll(resultList);
break;
}
return UserListState.builder().users(users).searchList(searchList).build();
});
events.compose(uiModelsTransformer)
.subscribe(uiModel -> {/* Your implementation here*/});

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