I have a function that takes an article id list to set on the adapter. Everything works fine until at least one of the requests fails. Then the returned list is empty. How to make it ignore a failing request and move on to the next one? For example, I request 5 articles 1 fail, 4 are okay, so I get a list of 4.
I know, I need to use onErrorResumeNext() here, but I don't know-how.
Interface:
#GET("articles/{id}")
Observable<Articles> getArticle1(#Path("id") int id);
Activity:
private void getMoreArticles(List<Integer> l) {
ApiInterface apiInterface = ApiClient.getApiClientRX().create(ApiInterface.class);
List<Observable<?>> requests = new ArrayList<>();
for (int id : l) {
requests.add(apiInterface.getArticle1(id));
}
Observable.zip(requests, new Function<Object[], List<Articles>>() {
#Override
public List<Articles> apply(#NonNull Object[] objects) {
List<Articles> articlesArrayList = new ArrayList<>();
for (Object response : objects) {
articlesArrayList.add((Articles) response);
}
return articlesArrayList;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorResumeNext(Observable.<List<Articles>>empty())
.subscribe(
new Consumer<List<Articles>>() {
#Override
public void accept(List<Articles> articlesList) {
adapter = new Adapter(articlesList, MainActivity.this);
if (fav) recyclerView.setAdapter(adapter);
else addRV().setAdapter(adapter);
adapter.notifyDataSetChanged();
initListener();
swipeRefreshLayout.setRefreshing(false);
}
},
new Consumer<Throwable>() {
#Override
public void accept(Throwable e) throws Exception {
}
}
).isDisposed();
}
I tried to simplify your use case a bit but I hope you got my point. You need to somehow "signal" that there was some problem in your API call and this specific Articles object should be skipped in your .zip() operator's zipper function. You can for example wrap the return value into Optional. When the value is preset, it indicates everything went fine. If not, the API call failed.
class SO69737581 {
private Observable<Articles> getArticle1(int id) {
return Observable.just(new Articles(id))
.map(articles -> {
if (articles.id == 2) { // 1.
throw new RuntimeException("Invalid id");
} else {
return articles;
}
});
}
Observable<List<Articles>> getMoreArticles(List<Integer> ids) {
List<Observable<Optional<Articles>>> requests = new ArrayList<>();
for (int id : ids) {
Observable<Optional<Articles>> articleRequest = getArticle1(id)
.map(article -> Optional.of(article)) // 2.
.onErrorReturnItem(Optional.empty()); // 3.
requests.add(articleRequest);
}
return Observable.zip(requests, objects -> {
List<Articles> articlesArrayList = new ArrayList<>();
for (Object response : objects) {
Optional<Articles> optionalArticles = (Optional<Articles>) response;
optionalArticles.ifPresent(articlesArrayList::add); // 4.
}
return articlesArrayList;
});
}
}
Explanation of interesting parts:
Simulate API error with id = 2
Wrap result of API the call into optional
Return empty optional when an error occurs
Add articles value into result array if the value is present
Verification:
public class SO69737581Test {
#Test
public void failedArticleCallsShouldBeSkipped() {
SO69737581 tested = new SO69737581();
TestObserver<List<Articles>> testSubscriber = tested
.getMoreArticles(Arrays.asList(1, 2, 3, 4))
.test();
List<Articles> result = Arrays.asList(
new Articles(1),
new Articles(3),
new Articles(4)
);
testSubscriber.assertComplete();
testSubscriber.assertValue(result);
}
}
For sake of completeness, this is how I defined Article class:
class Articles {
public int id;
public Articles(int id) {
this.id = id;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Articles articles = (Articles) o;
return id == articles.id;
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
I need to combine these two Data. They both have their own Fragment,Dao, Model and Repository. And both return different data from different tables.
ItemFavourite table stores id of the tables aboves Item and ItemMoto.
public LiveData<Resource<List<Item>>> getItemFavouriteData() {
return itemFavouriteData;
}
//Moto
public LiveData<Resource<List<ItemMoto>>> getItemFavouriteDataMoto() {
return itemFavouriteDataMoto;
}
This is how I tried it.
public class FavouriteViewModel extends PSViewModel {
private final LiveData<Resource<List<Item>>> itemFavouriteData;
private final LiveData<Resource<List<ItemMoto>>> itemFavouriteDataMoto;
private MutableLiveData<FavouriteViewModel.TmpDataHolder> itemFavouriteListObj = new
MutableLiveData<>();
private MutableLiveData<FavouriteMotoViewModel.TmpDataHolder> itemFavouriteListObjMoto = new
MutableLiveData<>();
#Inject
FavouriteViewModel(ItemRepository itemRepository, ItemMotoRepository itemMotoRepository) {
itemFavouriteData = Transformations.switchMap(itemFavouriteListObj, obj -> {
if (obj == null) {
return AbsentLiveData.create();
}
Utils.psLog("itemFavouriteData");
return itemRepository.getFavouriteList(Config.API_KEY, obj.userId, obj.offset);
});
itemFavouriteDataMoto = Transformations.switchMap(itemFavouriteListObjMoto, obj -> {
if (obj == null) {
return AbsentLiveData.create();
}
Utils.psLog("itemFavouriteData");
return itemMotoRepository.getFavouriteList(Config.API_KEY, obj.userId, obj.offset);
});
}
public LiveData<Resource<List<Item>>> getItemFavouriteData() {
return itemFavouriteData;
}
public LiveData<Resource<List<ItemMoto>>> getItemFavouriteDataMoto() {
return itemFavouriteDataMoto;
}
private static LiveData<Resource<List<Item>>> mergeDataSources(LiveData... sources) {
MediatorLiveData<Resource<List<Item>>> mergedSources = new MediatorLiveData();
for (LiveData source : sources) {
mergedSources.addSource(source, mergedSources::setValue);
}
return mergedSources;
}
public LiveData<Resource<List<Item>>> getFavourites() {
return mergeDataSources(
getItemFavouriteDataMoto(),
getItemFavouriteData());
}
}
From Fragment I observe the data like this:
LiveData<Resource<List<Item>>> news = favouriteViewModel.getFavourites();
if (news != null) {
news.observe(this, listResource -> {
if (listResource != null) {
switch (listResource.status) {
case LOADING:
// Loading State
// Data are from Local DB
if (listResource.data != null) {
//fadeIn Animation
fadeIn(binding.get().getRoot());
// Update the data
replaceData(listResource.data);
}
break;
case SUCCESS:
// Success State
// Data are from Server
if (listResource.data != null) {
// Update the data
replaceData(listResource.data);
}
favouriteViewModel.setLoadingState(false);
break;
case ERROR:
// Error State
favouriteViewModel.setLoadingState(false);
favouriteViewModel.forceEndLoading = true;
break;
default:
// Default
break;
}
} else {
// Init Object or Empty Data
if (favouriteViewModel.offset > 1) {
// No more data for this list
// So, Block all future loading
favouriteViewModel.forceEndLoading = true;
}
}
});
}
The only data I am getting are from Item table only.
Using mediator live data we can observe the 2 livedata.
val groupChatFeed: LiveData<List<Feed<*>>> = MediatorLiveData<List<Feed<*>>>().apply {
fun prepareDataAndSetStates(): List<Feed<*>> {
val data: MutableList<Feed<*>> = mutableListOf()
if (postList.value?.data?.isNullOrEmpty() == false) {
data.addAll(postList.value?.data ?: emptyList())
}
if (connectionRecommendations.value?.data?.isNullOrEmpty() == false) {
val recommendations = connectionRecommendations.value?.data?.toFeedItem()
data.add(recommendations)
}
return data
}
addSource(postList) {
value = prepareDataAndSetStates()
}
addSource(connectionRecommendations) {
value = prepareDataAndSetStates()
}
}
We are observing 2 different livedata postList and connectionRecommendations.
You can use MediatorLiveData and tuples, but you can technically also use this library I wrote for this specific purpose which does it for you, and solve it like this
import static com.zhuinden.livedatacombineutiljava.LiveDataCombineUtil.*;
private final LiveData<Pair<Resource<List<Item>>, Resource<List<ItemMoto>>>> favorites = combine(itemFavouriteData, itemFavouriteDataMoto, (favorites, favoriteMoto) -> {
return Pair.create(favorites, favoriteMoto);
});
public LiveData<Pair<Resource<List<Item>>, Resource<List<ItemMoto>>>> getFavorites() {
return favorites;
}
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)
(I was adding a comment but it was too long)
I cannot help with the talks and so on presented by Jake. But regarding your last question:
Does this pattern of UI Event ---> Action ---> Result ---> UI Model
still apply for cases where there is no "Backend" as such?
It does, it is just that the backend is your application state repository.
In this kind of architecture there should only be one place of truth for your application: be it a backend, a local database, a combination of both or whatever solution is appropriate for your usecase.
Having that in mind your Action streams should modify the state either by doing calls to the backend, posting changes to a database or writing elements in the sharedSetting. Similarly, changes in your state should trigger sending Results down your streams.
The specific details would depend on what you use as a source of truth for your application.
DataFlow and State
It uses mainly Paco and Jake Wharton RxState idea plus added some more stuff.
To use UiEvent → Action, Result → UiModel transformers and always act
on a single state with the help of RxJava operators (Forming a single
stream of events, then based on their types handling actions with
different transformers, then combine results again, modifying the
state and then finally render it on the UI.
or not to use transformers and make it a little bit “simpler”.
So here is the “full” view model code without using any transformers:
class SomeViewModel(private val someRepository: SomeRepository): ViewModel() {
val uiEvents: PublishSubject<UiEvent> = PublishSubject.create()
val outputState: MutableLiveData<Result<UiState>> = MutableLiveData()
init {
uiEvents.subscribe {
when (it) {
is FirstEvent -> getSomeResultsFromRepo(it.id)
is SecondEvent -> handleSecondEvent()
}
}
}
fun getSomeResultsFromRepo(id: String) {
someRepository.getResult(id)
.map { UiState(it) }
.map { Result.success(it) }
.startWith(Result.loading())
.onErrorReturn { handleError(it) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
outputState.postValue(it)
})
}
fun handleSecondEvent() {
/* do something here */
someRepository.getSomeOtherResult()
.map { UiState(it) }
.map { Result.success(it) }
.startWith(Result.loading())
.onErrorReturn { handleError(it) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
outputState.postValue(it)
})
}
private fun handleError(error: Throwable): Result<UiState> {
return if (error is RetrofitException) {
when (error.kind) {
RetrofitException.Kind.NETWORK -> Result.failure(NetworkError(error))
RetrofitException.Kind.HTTP -> Result.failure(ServerError(error))
RetrofitException.Kind.UNEXPECTED -> Result.failure(UnknownError(error))
else -> Result.failure(UnknownError(error))
}
} else {
Result.failure(UnknownError(error))
}
}
class Factory #Inject constructor(private val someRepo: SomeRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
#Suppress("UNCHECKED_CAST")
return SomeViewModel(someRepo) as T
}
}
}
As you can see here 2 streams: a uiEvents (1st stream) which gets all the input events from the UI. As long as the UI exists it will be catching those events. Based on the event types it calls some repository functions (use cases) that are returning some response and then they are updating the model (2nd stream) with one of the possible outcomes: Success, Error or Loading.
Also transform the errors in API to so called RetrofitErrors, and based on their type it can show different error messages to the User.
There is some duplication as well that can be avoided easily, but what I wanted to show here is that it always start with a Loading result, then either a Success or an Error.
One of the most important thing is that this way "To keep state in the stream, which is a LiveData."
One benefit of this setup (just like using a BehaviourSubject) is that it will always return the last state — on orientation change it is very useful as it just loads the last available state.
Also it is highly testable as each piece can be tested in separation with providing mocked repo or view and it is also very easy to debug as we always have a current state in the stream.
I have a ParseObject subclass , but everytime I want to get index of it it returns 0 so mListSectionPos returns an array of zero's (hachCode and equals methd implemented thanks to Apache Commons Utils).
It should be String.valueOf(mListItems.indexOf(beer_section)), but instead I'm using mListSectionPos.add(mListItems.indexOf(current_item) - 1); because it's working (more or less). Sometimes it cracks on getCurrentSectionPosition() that also works on indexOf() method.
So my question is: why indexOf() always return 0 in this piece of code?
It's based on https://github.com/bhavyahmehta/ListviewFilter - just adapted for ParseObject lists. Code below is my adaptation of his MainActivity.java that can be found here:
#Override
protected Void doInBackground(ArrayList<PiwoSubclass>... params) {
mListItems.clear();
mListSectionPos.clear();
ArrayList<PiwoSubclass> items = params[0];
if(mItems != null) {
if (mItems.size() > 0) {
String prev_section = "";
for (PiwoSubclass current_item : items) {
if (isCancelled()) break;
String current_section = current_item.getBeerName().substring(0, 1).toUpperCase(Locale.getDefault());
if (!prev_section.equals(current_section)) {
PiwoSubclass beer_section = null;
beer_section = new PiwoSubclass();
beer_section.setBeerName(current_section);
Log.i("ASD-current", beer_section.getBeerName());
mListItems.add(beer_section);
mListItems.add(current_item);
// array list of section positions
mListSectionPos.add(mListItems.indexOf(current_item) - 1); // that want works although it's way around
// TODO why is that error?
Log.i("ASD-listSectionSize", String.valueOf(mListItems.indexOf(beer_section)));
prev_section = current_section;
} else {
mListItems.add(current_item);
}
}
}
}
return null;
}
PiwoSubclass
public class PiwoSubclass extends ParseObject {
private String objectIdP;
private String marka;
private String marka_lowercase;
public PiwoSubclass() {
}
public String getObjectIdfromParse() {
return this.getObjectId();
}
public String getMarka(){
return this.getString("marka");
}
public String getBrewery(){
return this.getString("brewery");
}
public String getBeerName(){
return this.getString("beer_name");
}
public String getMarka_lowercase() {
return this.getString("marka_lowercase");
}
public void setMarka(String value){
put("marka", value);
}
public void setBeerName(String value){
put("beer_name", value);
}
public void setMarka_lowercase(String value){
put("marka_lowercase", value);
}
#Override
public int hashCode() {
return new HashCodeBuilder(17, 31) // two randomly chosen prime numbers
// if deriving: appendSuper(super.hashCode()).
.append(getObjectIdfromParse())
.toHashCode();
}
#Override
public boolean equals(Object obj) {
//return super.equals(obj);
if (!(obj instanceof PiwoSubclass))
return false;
if (obj == this)
return true;
marka_lowercase = getMarka_lowercase();
PiwoSubclass rhs = (PiwoSubclass) obj;
//Log.i("ASD-subclass", marka + "/" + rhs.getMarka());
return new EqualsBuilder()
// if deriving: appendSuper(super.equals(obj)).
.append(marka_lowercase, rhs.getMarka_lowercase())
.isEquals();
}
Now I have IndexOutOfBounds exception from PinnedHeaderAdapter:
public int getCurrentSectionPosition(int position) {
//String listChar = mListItems.get(position).getBeerName().substring(0, 1).toUpperCase(Locale.getDefault());
PiwoSubclass ps = mListItems.get(position); // TODO errorrrrrrrrr
return mListItems.indexOf(ps);
}
First, you check for mItems
if(mItems != null) {
if (mItems.size() > 0) {
but then you work with items
for (PiwoSubclass current_item : items) {
/* ... */
}
and ignore mItems for the rest of the method. I don't see any connection between these two.
It seems indexOf() doesn't return 0 but 1, otherwise you would get an ArrayList full of -1s
mListSectionPos.add(mListItems.indexOf(current_item) - 1);
I guess, somehow you always check for the first current_item, which is the second element in mListItems. If you would check for the beer_section - as it does for current_section in the original code - the code would work as expected.
After looking into ArrayList.indexOf(), the most likely reason is your PiwoSubclass.equals() method compares always equal to the first non-section element, because it hasn't set a beer name or some similar condition.
So, fixing the equals method might work as well.
I'm trying to create a custom loader which loads an list of data that works fine but now I want to added endless scrolling in the listview. I thought a logical place would be in the loader since almost all the examples I see on the interwebz have a private field in the custom loader which corresponds with the data to be returned to UI and in the deliverResult there is some code like this
#Override
public void deliverResult(T data) {
T oldData = mData;
mData = data;
if (isStarted()) {
// If the loader is currently started, we can immediately deliver a result
super.deliverResult(mData);
}
}
No I thought that mData still contains the previous list [1,2,3,4,5] cause the loader should cache the data to show it instantaneously on configuration changes. And data is the new list [6,7,8,9,10] for instance. I could just add data to mData, mData.add(data) and we are done. Don't have to repeat the code on multiple places or different adapters. But seemingly this doesn't work, everytime you call restartLoader to load the new data the framework creates a new instance of the Loader. Has anyone else run into this problem before? or should I just do the mData.add(data) in the Adapter or somewhere else in the code.
the full implementation of the custom loader which extends ApiResponseLoader which can also be find below:
public class SearchLoader extends ApiResponseLoader {
private SearchType mSearchType;
private int mOffset;
private String mSearchQuery;
public SearchLoader(Context context, SearchType type, int offset, String query) {
super(context);
mSearchType = type;
mOffset = offset;
mSearchQuery = query;
}
#Override
public ApiResponse loadInBackground() {
try {
Map<String, String> parameters = Utils.parametersMap("q:" + mSearchQuery, "offset:" + String.valueOf(mOffset));
return tryLoadInBackground(parameters);
} catch (Exception e) {
setError(e);
return null;
}
}
public ApiResponse tryLoadInBackground(Map<String, String> parameters) throws Exception {
if (mSearchQuery == null) {
throw new NullPointerException("mSearchQuery should not be null");
}
if (mSearchType == SearchType.A) {
return RestAdapter().searchA(parameters);
} else {
return RestAdapter().searchB(parameters);
}
}
}
public abstract class ApiResponseLoader extends AsyncTaskLoader<ApiResponse> {
private final static String TAG = ApiResponseLoader.class.getSimpleName();
private ApiResponse mApiResponse;
private Exception mError;
public ApiResponseLoader(Context context) {
super(context);
}
public abstract ApiResponse tryLoadInBackground(Map<String, String> parameters) throws Exception;
#Override
protected void onStartLoading() {
if (mApiResponse != null) {
deliverResult(mApiResponse);
}
if (takeContentChanged() || mApiResponse == null) {
forceLoad();
}
}
#Override
protected void onForceLoad() {
super.onForceLoad();
}
#Override
protected void onStopLoading() {
cancelLoad();
}
#Override
public void onCanceled(ApiResponse data) {
// Attempt to cancel the current asynchronous load.
super.onCanceled(data);
}
#Override
protected void onReset() {
// Ensure the loader has been stopped.
onStopLoading();
// At this point we can release the resources associated with 'apps' if needed
if (mApiResponse != null) {
mApiResponse = null;
}
}
#Override
public void deliverResult(ApiResponse data) {
if (isReset()) {
// An async query came in while the loader is stopped. We don't need the result
if (data != null) {
onReleaseResources(data);
}
return;
}
if (mApiResponse != null) {
mApiResponse.mMeta = data.mMeta;
mApiResponse.mSampleList.addAll(data.mSampleList);
} else {
mApiResponse = data;
}
if (isStarted()) {
// If the loader is currently started, we can immediately deliver a result
super.deliverResult(mApiResponse);
}
}
public Exception getError() {
return mError;
}
public void setError(Exception mError) {
this.mError = mError;
}
}