I am attempting to developed an Android application based on the following talk presented by Jake Wharton
The State of Managing State with RxJava
21 March 2017 – Devoxx (San Jose, CA, USA)
Jake promised a part 2 and/or GITHUB example which I am unable to find (If indeed either exists)
At a high level I can follow/understand the majority of the above talk.
However I have the following questions.
I can see how employing UiEvent, UiModel, Action, and Result keeps concerns separated.
What I am confused about is the following:-
The diagram on slide 194 shows the "flow/stream" of Observables as
Android Device -----> Observable<UiEvent> -----> <application code> -----> Observable<Action> -----> {Backend}
{Backend} -----> Observable<Result> -----> <application code> -----> Observable<UiModel> -----> Android Device
Slide 210 contains this code snippet, showing how the Result(s) stream is "scan"ned into UiModel
SubmitUiModel initialState = SubmitUiModel.idle();
Observable<Result> results = /* ... */;
Observable<SubmitUiModel> uiModels = results.scan(initialState, (state, result) -> {
if (result == CheckNameResult.IN_FLIGHT
|| result == SubmitResult.IN_FLIGHT)
return SubmitUiModel.inProgress();
if (result == CheckNameResult.SUCCESS)
return SubmitUiModel.idle();
if (result == SubmitResult.SUCCESS)
return SubmitUiModel.success();
// TODO handle check name and submit failures...
throw new IllegalArgumentException("Unknown result: " + result);
});
and the final code snippet on slide 215, the code snippet resembles this:-
ObservableTransformer<SubmitAction, SubmitResult> submit =
actions -> actions.flatMap(action -> service.setName(action.name)
.map(response -> SubmitResult.SUCCESS)
.onErrorReturn(t -> SubmitResult.failure(t.getMessage()))
.observeOn(AndroidSchedulers.mainThread())
.startWith(SubmitResult.IN_FLIGHT));
ObservableTransformer<CheckNameAction, CheckNameResult> checkName =
actions -> actions.switchMap(action -> action
.delay(200, MILLISECONDS, AndroidSchedulers.mainThread())
.flatMap(action -> service.checkName(action.name))
.map(response -> CheckNameResult.SUCCESS)
.onErrorReturn(t -> CheckNameResult.failure(t.getMessage()))
.observeOn(AndroidSchedulers.mainThread())
.startWith(CheckNameResult.IN_FLIGHT));
which illustrates conversion from Action(s) to Result(s)
what am I missing from this talk/slide-deck on how to combine the UiEvent/UiModel to the Action/Result stream?
The stream is driven by UiEvents How do you complete the flow from UiEvent(s) to Action back to Result then finally UiModel?
UPDATE Using the Star Wars API I have taken the following approach I use my UI Events to drive the transformation between UI Events to Results via Actions, then scan the results to map back to UI Model.
Heres my classes and code:-
ACTION CLASSES
==============
public abstract class Action<T> {
Api service = Service.instance();
final T data;
public Action(final T data) {
this.data = data;
}
public T getData() {
return data;
}
public abstract Observable<Response<String>> execute();
}
public class CheckCharacterAction extends Action<String> {
public CheckCharacterAction(final String characterName) {
super(characterName);
}
@Override
public Observable<Response<String>> execute() {
return service.peopleSearch(getData());
}
}
public class CheckFilmAction extends Action<String> {
public CheckFilmAction(final String filmTitle) {
super(filmTitle);
}
@Override
public Observable<Response<String>> execute() {
return service.filmSearch(getData());
}
}
public class SearchAction extends Action<String> {
public SearchAction(final String search) {
super(search);
}
@Override
public Observable<Response<String>> execute() {
return service.filmSearch(getData());
}
}
EVENT CLASSES
=============
public abstract class UiEvent<T> {
private final T data;
public UiEvent(final T data) {
this.data = data;
}
public T getData() {
return data;
}
}
public class CharacterUiEvent extends UiEvent<String> {
public CharacterUiEvent(final String name) {
super(name);
}
}
public class FilmUiEvent extends UiEvent<String> {
public FilmUiEvent(final String title) {
super(title);
}
}
public class SearchUiEvent extends UiEvent<String> {
public SearchUiEvent(final String data) {
super(data);
}
}
UI MODEL CLASSES
================
public class UiModel<T> {
public final boolean isProgress;
public final String message;
public final boolean isSuccess;
public T data;
public UiModel(final boolean isProgress) {
this.isProgress = isProgress;
this.message = null;
this.isSuccess = false;
this.data = null;
}
public UiModel(final T data) {
this.isProgress = false;
this.message = null;
this.isSuccess = true;
this.data = data;
}
public UiModel(final String message) {
this.isProgress = false;
this.message = message;
this.isSuccess = false;
this.data = null;
}
public UiModel(final boolean isProgress, final String message, final boolean isSuccess, final T data) {
this.isProgress = isProgress;
this.message = message;
this.isSuccess = isSuccess;
this.data = data;
}
}
public class CharacterUiModel extends UiModel<JsonData> {
public CharacterUiModel(final boolean isProgress) {
super(isProgress);
}
public CharacterUiModel(final JsonData data) {
super(data);
}
public CharacterUiModel(final String message) {
super(message);
}
public CharacterUiModel(final boolean isProgress, final String message, final boolean isSuccess, final JsonData data) {
super(isProgress, message, isSuccess, data);
}
public static CharacterUiModel inProgress() {
return new CharacterUiModel(true);
}
public static CharacterUiModel success(final JsonData data) {
return new CharacterUiModel(data);
}
public static CharacterUiModel failure(final String message) {
return new CharacterUiModel(message);
}
}
public class FilmUiModel extends UiModel<JsonData> {
public FilmUiModel(final boolean isProgress) {
super(isProgress);
}
public FilmUiModel(final JsonData data) {
super(data);
}
public FilmUiModel(final String message) {
super(message);
}
public FilmUiModel(final boolean isProgress, final String message, final boolean isSuccess, final JsonData data) {
super(isProgress, message, isSuccess, data);
}
public static FilmUiModel inProgress() {
return new FilmUiModel(true);
}
public static FilmUiModel success(final JsonData data) {
return new FilmUiModel(data);
}
public static FilmUiModel failure(final String message) {
return new FilmUiModel(message);
}
}
public class SearchUiModel extends UiModel<JsonData> {
private SearchUiModel(final boolean isProgress) {
super(isProgress);
}
private SearchUiModel(final JsonData data) {
super(data);
}
private SearchUiModel(final String message) {
super(message);
}
private SearchUiModel(final boolean isProgress, final String message, final boolean isSuccess, final JsonData data) {
super(isProgress, message, isSuccess, data);
}
public static SearchUiModel idle() {
return new SearchUiModel(false, null, false, null);
}
public static SearchUiModel inProgress() {
return new SearchUiModel(true);
}
public static SearchUiModel success(final JsonData data) {
return new SearchUiModel(data);
}
public static SearchUiModel failure(final String message) {
return new SearchUiModel(message);
}
}
RESULT CLASSES
==============
public abstract class Result<T> {
public enum LIFECYCLE {
DEPARTURE_LOUNGE,
IN_FLIGHT,
LANDED_SAFELY,
CRASHED_BURNED
}
final LIFECYCLE lifecycle;
final T data;
final String errorMessage;
public Result(final LIFECYCLE lifecycle, final T data, final String errorMessage) {
this.lifecycle = lifecycle;
this.data = data;
this.errorMessage = errorMessage;
}
public T getData() {
return data;
}
public String getErrorMessage() {
return errorMessage;
}
public LIFECYCLE getLifecycle() {
return lifecycle;
}
}
public class CharacterResult extends Result<JsonData> {
private CharacterResult(final LIFECYCLE lifecycle, final JsonData data, final String errorMessage) {
super(lifecycle, data, errorMessage);
}
private CharacterResult(final LIFECYCLE lifecycle) {
super(lifecycle, null, null);
}
public static CharacterResult departureLounge() {
return new CharacterResult(LIFECYCLE.DEPARTURE_LOUNGE);
}
public static CharacterResult inflight() {
return new CharacterResult(LIFECYCLE.IN_FLIGHT);
}
public static CharacterResult landedSafely(final JsonData data) {
return new CharacterResult(LIFECYCLE.LANDED_SAFELY, data, null);
}
public static CharacterResult crashedBurned(final String errorMessage) {
return new CharacterResult(LIFECYCLE.CRASHED_BURNED, null, errorMessage);
}
}
public class FilmResult extends Result<JsonData> {
private FilmResult(final LIFECYCLE lifecycle, final JsonData data, final String errorMessage) {
super(lifecycle, data, errorMessage);
}
private FilmResult(final LIFECYCLE lifecycle) {
super(lifecycle, null, null);
}
public static FilmResult departureLounge() {
return new FilmResult(LIFECYCLE.DEPARTURE_LOUNGE);
}
public static FilmResult inflight() {
return new FilmResult(LIFECYCLE.IN_FLIGHT);
}
public static FilmResult landedSafely(final JsonData data) {
return new FilmResult(LIFECYCLE.LANDED_SAFELY, data, null);
}
public static FilmResult crashedBurned(final String errorMessage) {
return new FilmResult(LIFECYCLE.CRASHED_BURNED, null, errorMessage);
}
}
public class SearchResult extends Result<JsonData> {
private SearchResult(final LIFECYCLE lifecycle, final JsonData data, final String errorMessage) {
super(lifecycle, data, errorMessage);
}
private SearchResult(final LIFECYCLE lifecycle) {
super(lifecycle, null, null);
}
public static SearchResult departureLounge() {
return new SearchResult(LIFECYCLE.DEPARTURE_LOUNGE);
}
public static SearchResult inflight() {
return new SearchResult(LIFECYCLE.IN_FLIGHT);
}
public static SearchResult landedSafely(final JsonData data) {
return new SearchResult(LIFECYCLE.LANDED_SAFELY, data, null);
}
public static SearchResult crashedBurned(final String errorMessage) {
return new SearchResult(LIFECYCLE.CRASHED_BURNED, null, errorMessage);
}
}
I then set up my Rx Streams as follows from my Activity onCreate()
method:-
final Observable<SearchUiEvent> searchEvents = RxView.clicks(activityMainBinding.searchButton)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.map(ignored -> new SearchUiEvent(activityMainBinding.filmTitle.getText().toString()));
final Observable<FilmUiEvent> filmEvents = RxTextView.afterTextChangeEvents(activityMainBinding.filmTitle)
.skipInitialValue()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.delay(1000, MILLISECONDS, AndroidSchedulers.mainThread())
.map(text -> new FilmUiEvent(text.view().getText().toString()));
final Observable<CharacterUiEvent> characterEvents = RxTextView.afterTextChangeEvents(activityMainBinding.people)
.skipInitialValue()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.delay(200, MILLISECONDS, AndroidSchedulers.mainThread())
.map(text -> new CharacterUiEvent(text.view().getText().toString()));
/**
*
*/
final Observable<UiEvent> uiEvents = Observable.merge(searchEvents, filmEvents, characterEvents);
/*********
*
*/
final ObservableTransformer<SearchUiEvent, SearchResult> searchAction =
events -> events.flatMap(event -> new SearchAction(event.getData()).execute().subscribeOn(Schedulers.io()))
.map(response -> SearchResult.landedSafely(new JsonData(response.body())))
.onErrorReturn(throwable -> SearchResult.crashedBurned(throwable.getMessage()))
.startWith(SearchResult.inflight());
final ObservableTransformer<FilmUiEvent, FilmResult> filmAction =
events -> events.flatMap(event -> new CheckFilmAction(event.getData()).execute().subscribeOn(Schedulers.io()))
.map(response -> FilmResult.landedSafely(new JsonData(response.body())))
.onErrorReturn(throwable -> FilmResult.crashedBurned(throwable.getMessage()))
.startWith(FilmResult.inflight());
final ObservableTransformer<CharacterUiEvent, CharacterResult> characterAction =
events -> events.flatMap(event -> new CheckCharacterAction(event.getData()).execute().subscribeOn(Schedulers.io()))
.map(response -> CharacterResult.landedSafely(new JsonData(response.body())))
.onErrorReturn(throwable -> CharacterResult.crashedBurned(throwable.getMessage()))
.startWith(CharacterResult.inflight());
final ObservableTransformer<UiEvent, ? extends Result> whatever = events -> events.publish(shared -> Observable.merge(
shared.ofType(SearchUiEvent.class).compose(searchAction),
shared.ofType(CharacterUiEvent.class).compose(characterAction),
shared.ofType(FilmUiEvent.class).compose(filmAction)));
/**
*
*/
final UiModel initialState = SearchUiModel.idle();
final Observable<? extends Result> results = uiEvents.compose(whatever).doOnSubscribe(COMPOSITE_DISPOSABLE::add);
final Observable<UiModel> models = results.scan(initialState, (state, result) -> {
Log.e(TAG, "scan() state = " + state + " result = " + result);
if (result.getLifecycle().equals(SearchResult.LIFECYCLE.DEPARTURE_LOUNGE) ||
result.getLifecycle().equals(CharacterResult.LIFECYCLE.DEPARTURE_LOUNGE) ||
result.getLifecycle().equals(FilmResult.LIFECYCLE.DEPARTURE_LOUNGE)) {
return SearchUiModel.idle();
}
if (result.getLifecycle().equals(SearchResult.LIFECYCLE.IN_FLIGHT) ||
result.getLifecycle().equals(CharacterResult.LIFECYCLE.IN_FLIGHT) ||
result.getLifecycle().equals(FilmResult.LIFECYCLE.IN_FLIGHT)) {
return SearchUiModel.inProgress();
}
if (result.getLifecycle().equals(SearchResult.LIFECYCLE.LANDED_SAFELY) ||
result.getLifecycle().equals(CharacterResult.LIFECYCLE.LANDED_SAFELY) ||
result.getLifecycle().equals(FilmResult.LIFECYCLE.LANDED_SAFELY)) {
return SearchUiModel.success((JsonData) result.getData());
}
if (result.getLifecycle().equals(SearchResult.LIFECYCLE.CRASHED_BURNED) ||
result.getLifecycle().equals(CharacterResult.LIFECYCLE.CRASHED_BURNED) ||
result.getLifecycle().equals(FilmResult.LIFECYCLE.CRASHED_BURNED)) {
return SearchUiModel.failure(result.getErrorMessage());
}
return null;
});
models.doOnSubscribe(COMPOSITE_DISPOSABLE::add).subscribe(model -> report(model), throwable -> error(throwable));
As soon as my activity displays I get the following logs:-
2018-10-09 14:22:33.310 D/MainActivity: report() called with: model = [UiModel{isProgress=false, message='null', isSuccess=false, data=null}]
2018-10-09 14:22:33.311 E/MainActivity: scan() state = UiModel{isProgress=false, message='null', isSuccess=false, data=null} result = SearchResult{lifecycle=IN_FLIGHT, data=null, errorMessage='null'}
2018-10-09 14:22:33.311 D/MainActivity: report() called with: model = [UiModel{isProgress=true, message='null', isSuccess=false, data=null}]
2018-10-09 14:22:33.313 E/MainActivity: scan() state = UiModel{isProgress=true, message='null', isSuccess=false, data=null} result = CharacterResult{lifecycle=IN_FLIGHT, data=null, errorMessage='null'}
2018-10-09 14:22:33.313 D/MainActivity: report() called with: model = [UiModel{isProgress=true, message='null', isSuccess=false, data=null}]
2018-10-09 14:22:33.313 E/MainActivity: scan() state = UiModel{isProgress=true, message='null', isSuccess=false, data=null} result = FilmResult{lifecycle=IN_FLIGHT, data=null, errorMessage='null'}
2018-10-09 14:22:33.313 D/MainActivity: report() called with: model = [UiModel{isProgress=true, message='null', isSuccess=false, data=null}]
Im guessing I get these IN FLIGHT
results due to my .startWith()
statements.
When I either click my Search button or enter any text in my EditText views I see the following logs:-
2018-10-09 14:55:19.463 E/MainActivity: scan() state = UiModel{isProgress=false, message='null', isSuccess=true, data=com.test.model.JsonData@5e0b6f1} result = FilmResult{lifecycle=LANDED_SAFELY, data=com.test.model.JsonData@8ae4d86, errorMessage='null'}
2018-10-09 14:55:19.463 D/MainActivity: report() called with: model = [UiModel{isProgress=false, message='null', isSuccess=true, data=com.test.model.JsonData@8ae4d86}]
Why do I not see "IN FLIGHT" then "LANDED SAFELY"?
I only get "LANDED SAFELY"
Is my approach to transforming between UI Event -> Action -> Result -> UI Model anywhere close to what is described by Mr J Wharton?
Where have I gone wrong?
UPDATE (II)
My mistake was to not include all my downstream Rx within the .flatmap()
operation.
CLARIFICATION
Does this pattern of UI Event ---> Action ---> Result ---> UI Model still apply for cases where there is no "Backend" as such? e.g. a Home screen could present the user with a number of options (buttons) to navigate to lower level screens within the application. The UI Event would be "Button Click" the UI Model would return with the associated Activity class to employ with the startActivity()
method call.
How can I amalgamate the UI input events of a login screen into a single stream of UI events where I have two EditText fields (User Name and Password) and a Login Button. I would want the button click UI event to contain the user name and user password entered. If I was using RxBinding to process the EditTexts and the Login button click I cannot see how I can combine these three Observables into my UI event stream and have the EditTexts validated to ensure they have data entered and then pass this user entered data to my back end login API (or maybe Google Sign In for example)