I am using Retrofit, Live data. There is one situation on my project, I have to make sequence of network call. if any one fails it should return error.
At present I have two live data observers to get the work done, which is not good approach so I wanted to know the better approach or sample code to handle such requirement.
Note: I am not using Rxjava.
View code Basic logic
String id = "items/1233"; //ID which has to to be deleted
if (isCustomizedItem) {
viewModel.deleteEvent(id);
} else {
viewModel.createCustomItems();
viewModel.deleteEvent(id);
}
Livedata observers
viewModel.getItemDeleted().observe(this, serverResponse -> {
if (serverResponse.status == Status.SUCCESS) {
Timber.i("Successfully deleted");
}
});
viewModel.itemCreated().observe(this, serverResponse -> {
if (serverResponse.status == Status.SUCCESS) {
Timber.i("new items added");
//Again call delete for specific item
viewModel.deleteEvent(id);
}
});
Viewmodel code
createItems = Transformations.switchMap(eventData, (data) -> {
if (canCreateItems(data)) {
return AbsentLiveData.create();
} else {
return eventItemRepository.createItems();
}
});
deleteItem = Transformations.switchMap(deleteItem, (item) -> {
if (!isValidItem(item)) {
return AbsentLiveData.create();
} else {
return eventItemRepository.deleteItem(item);
}
});
Repo code.
public LiveData<Resource<List<Items>>> createItems() {
return new NetworkBoundResource<List<Items>> (executors) {
#NonNull
#Override
protected LiveData<ApiResponse<List<Items>>> createCall() {
return services.createItems();
}
}.asLiveData();
}
public LiveData<Resource<EmptyResponse>> deleteItem(String id) {
return new NetworkBoundResource<EmptyResponse> (executors) {
#NonNull
#Override
protected LiveData<ApiResponse<EmptyResponse>> createCall() {
return services.deleteItem(id);
}
}.asLiveData();
}
Service interface.
#GET(Constants.API_PATH+"/createitems/")
LiveData<ApiResponse<List<Items>>> createItems();
#GET(Constants.API_PATH+"/delete/{id}")
LiveData<ApiResponse<EmptyResponse>> deleteItem(#Path("id") String id);
I want to call createItems and deleteItem together. How can i achieve this?
Finally I write the solution. I used Mediatorlivedata to observe livedata changes on viewmodel.
Method which is responsible for both network call
public LiveData<Resource<EmptyResponse>> updateEvent(RequestCustomEvent request) {
return new UpdateItineraryRequests<EmptyResponse>(request).asLiveData();
}
and a class which will observe live data changes on viewmodel.
private class UpdateItineraryRequests<RequestType> {
private final MediatorLiveData<Resource<RequestType>> result = new MediatorLiveData<>();
UpdateItineraryRequests(RequestCustomEvent request) {
startExecution(request);
}
void startExecution(RequestCustomEvent request) {
//First check the its custom or not if its custom then directly change.
if (request.isCustom()) {
LiveData<Resource<EmptyResponse>> observable = repo.deleteItem(request.getEventID());
result.addSource(observable, response -> {
result.removeSource(observable);
if (response.status == Status.SUCCESS) {
result.setValue(Resource.success(null));
} else {
result.setValue(Resource.error("unable to delete", null));
}
});
} else {
LiveData<Resource<List<Items>>> itemsObservable = repo.createItems(request.getDataToChange());
result.addSource(itemsObservable, response -> {
result.removeSource(itemsObservable);
LiveData<Resource<EmptyResponse>> observable = repo.deleteItem(request.getEventID());
result.addSource(observable, response -> {
result.removeSource(observable);
if (response.status == Status.SUCCESS) {
//Do rest of network calls
}
}
});
}
}
LiveData<Resource<RequestType>> asLiveData() {
return result;
}
}
Related
Help me please to run rxJava code sample for android. Here is what I want to do:
I have a function, which gives me redirects one by one. I need to take that redirect (if it's not empty) and try to startActivity of external app with it. If redirect empty - I need to break and return some Result data with last redirect found. If this app started successfully - I need to return some Result object that tells that app have been opened, if not opened - I need to try to get next redirect. Here is code that I have:
public Observable<Result> getStartAppResult(String url, AppOpeningRule shopOpeningRule) {
return getSingleHttpRedirectUrl(url, appOpeningRule)
.flatMap(redirect -> {
if(Strings.isNullOrEmpty(redirect)){
return isAppStarted(redirect, appOpeningRule.getPackageName());
} else {
return getDataSourceWithParams(redirect);
}
})
.flatMap(appStarted -> {
if(appStarted){
return getDataSourceWithParams(redirect);
} else {
return getSingleHttpRedirectUrl(url, appOpeningRule);
}
});
}
private ObservableSource<Result> getDataSourceWithParams(String finalUrl) {
Result result = new Result(finalUrl, lastRedirectUrl);
return Observable.just(result);
}
public Observable<Boolean> isAppStarted(String url, String appPackageName) {
return Observable.create(emitter -> {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER);
intent.setPackage(appPackageName);
boolean hasActivityNotFoundException = false;
try {
context.startActivity(intent);
} catch (ActivityNotFoundException exception) {
hasActivityNotFoundException = true;
} finally {
emitter.onNext(!hasActivityNotFoundException);
emitter.onComplete();
}
});
}
private Observable<String> getSingleHttpRedirectUrl(String url,
final ShopOpeningRule appOpeningRule) {
return httpService.goToWebSiteLink(url)
.compose(rxTransformers.applyUnauthorizedHandlerCustomError())
.onErrorResumeNext(throwable -> {
if (throwable instanceof RetrofitException) {
RetrofitException error = (RetrofitException) throwable;
if (error.getKind() == Kind.HTTP) {
if (error.getHttpExceptionCode() >= REDIRECT_START && error.getHttpExceptionCode() <= REDIRECT_END) {
String redirectUrl = error.getResponse().headers().get(KEY_HEADER_REDIRECT_LOCATION);
if (!Strings.isNullOrEmpty(redirectUrl)) {
lastRedirectUrl = redirectUrl;
if (appOpeningRule.hasRedirectLinkPatterns()) {
if (LinkParser.urlContainsPattern(redirectUrl, appOpeningRule.getRedirectLinkPatterns())) {
String modifiedLink = LinkParser.modifyRedirectLinkIfNeeded(redirectUrl, shopOpeningRule);
return Observable.just(modifiedLink);
} else {
return Observable.just(redirectUrl);
}
} else {
return Observable.just(redirectUrl);
}
}
}
}
}
return Observable.just("");
});
}
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 have a fragment in my app that i show two list of saparate data in it.i'm using from android architecture components to load my data.
Once the data is fetched from the network, I store it locally using Room DB and then display it on the UI using ViewModel that observes on the LiveData object (this works fine). However, I want to be able to have a refreshLayout which When Refreshing Occurs a refresh action and perform a network request to get new data from the API if and only if there is a network connection.The issue is when Refreshing Occurs data load from locate database and network together .
my question is :How do I manage to get data only from Network when refreshing data?
How do I manage to get data only from Network when refreshing data?
I've seen this question and it didn't help me...
my codes:
repository:
public NetworkResult<LiveData<HomeHealthModel>> getHomeHealth(String query) {
MutableLiveData<String> _liveError = new MutableLiveData<>();
MutableLiveData<HomeHealthModel> data = new MutableLiveData<>();
LiveData<List<GeneralItemModel>> liveClinics = App.getDatabase().getGeneralItemDAO().getTops(GeneralItemType.Clinics, GeneralItemType.TOP);
LiveData<List<GeneralItemModel>> liveDoctors = App.getDatabase().getGeneralItemDAO().getTops(GeneralItemType.Doctors, GeneralItemType.TOP);
setupService(_liveError); //request data from network
data.postValue(new HomeHealthModel(liveClinics, liveDoctors));
_liveError.postValue(String.valueOf(NetworkResponseType.LocaleData));
return new NetworkResult<>(_liveError, data);
}
my viewModel
public class HomeHealthVM extends ViewModel {
private MutableLiveData<String> queryLiveData;
private LiveData<String> networkErrors;
private LiveData<List<GeneralItemModel>> Clinics;
private LiveData<List<GeneralItemModel>> Doctors;
public HomeHealthVM(HealthRepository repository) {
queryLiveData = new MutableLiveData<>();
LiveData<NetworkResult<LiveData<HomeHealthModel>>> repoResult;
repoResult = Transformations.map(queryLiveData, repository::getHomeHealth);
LiveData<HomeHealthModel> model = Transformations.switchMap(repoResult, input -> input.data);
Doctors = Transformations.switchMap(model, HomeHealthModel::getDoctors);
Clinics = Transformations.switchMap(model, HomeHealthModel::getClinics);
networkErrors = Transformations.switchMap(repoResult, input -> input.error);
}
public void search(String queryString) {
queryLiveData.postValue(queryString);
}
public String lastQueryValue() {
return queryLiveData.getValue();
}
public LiveData<String> getNetworkErrors() {
return networkErrors;
}
public LiveData<List<GeneralItemModel>> getClinics() {
return Clinics;
}
public LiveData<List<GeneralItemModel>> getDoctors() {
return Doctors;
}
}
my fragment code:
private void setupViewModel() {
ViewModelFactory<HealthRepository> factory = new ViewModelFactory<>(new HealthRepository());
healthVM = ViewModelProviders.of(this, factory).get(HomeHealthVM.class);
healthVM.getNetworkErrors().observe(this, states -> {
try {
if (Integer.parseInt(states) != WarningDialogType.Success &&
Integer.parseInt(states) != WarningDialogType.Locale) {
stopLoading();
linerNoInternet.setVisibility(View.VISIBLE);
linerContent.setVisibility(View.GONE);
}
} catch (Exception e) {
stopLoading();
linerNoInternet.setVisibility(View.VISIBLE);
linerContent.setVisibility(View.GONE);
}
});
healthVM.getDoctors().observe(this, doctors -> {
if (doctors.size() > 0) {
doctorsAdapter.submitList(doctors);
stopLoading();
} else {
}
});
healthVM.getClinics().observe(this, clinics -> {
if (clinics.size() > 0) {
clinicsAdapter.submitList(clinics);
stopLoading();
} else {
conesClinics.setVisibility(View.GONE);
}
});
healthVM.search("");
}
I am new to RxJava. I have a scenario where I want to call first login webservice (getLoginObservable) and on success, want to call another webservice (getFetchDataObservable) to get user information.
I have following code working if login is success. But I am unable to figure out how to code failure case.
private void doLogin() {
emailAddress = editTextUsername.getText().toString();
final String password = editTextPassword.getText().toString();
showProgress(null, getString(R.string.loggingInPleaseWait));
getLoginObservable(editTextUsername.getText().toString(), password)
.map(response -> {
if (response.result) {
getPresenter().saveUserDetails(getContext(), emailAddress, true, response.dataObject.questionId, response.dataObject.question);
}
return response;
})
.flatMap(response -> {
return getFetchDataObservable();
})
.subscribe(res -> {
dismissProgress();
if (res.result) {
saveInformation(password, res);
} else {
ConstantsMethods.showOkButtonDialog(getContext(), res.message, null);
}
}, e -> {
dismissProgress();
if (e instanceof NoInternetConnectionException) {
ConstantsMethods.showOkButtonDialog(getContext(), getString(R.string.noInternetConnection), null);
}
Log.e(LoginFragment.class.getSimpleName(), e.getMessage());
});
}
private Observable<WsResponse<SecurityQuestion>> getLoginObservable(String userName, String password) {
return Observable.<WsResponse<SecurityQuestion>>create(subscriber -> {
getPresenter().doLogin(getActivity(), userName, password, appType,
new Callback<Void, WsResponse<SecurityQuestion>>() {
#Override
public Void callback(final WsResponse<SecurityQuestion> param) {
subscriber.onNext(param);
return null;
}
});
});
}
private Observable<WsResponse<PatientDataProfile>> getFetchDataObservable() {
return Observable.create(subscriber -> {
new AfPatientsPresenter().fetchPatientData(getContext(), emailAddress, "", new Callback<Void, WsResponse<PatientDataProfile>>() {
#Override
public Void callback(WsResponse<PatientDataProfile> param1) {
subscriber.onNext(param1);
subscriber.onComplete();
return null;
}
});
});
}
As much i know RxJava, I can figure out that getLoginObservable(editTextUsername.getText().toString(), password) observable send response to map (map(response -> { ... }) and this map return response to flatmap (flatMap(response -> { ... }) and its response is sent to subscriber. Here i am just lost that how can i skip (second network call)flatmap flatMap(response -> { ... } to send response directly to subscriber in case of login failure.
instead of:
.map(response -> {
if (response.result) {
getPresenter().saveUserDetails(getContext(), emailAddress, true, response.dataObject.questionId, response.dataObject.question);
}
return response;
})
you can use:
flatMap(response-> {
if (response.result) {
getPresenter().saveUserDetails(getContext(), emailAddress, true, response.dataObject.questionId, response.dataObject.question);
return Observable.just(response);
} else {
return Observable.error(new Exception("Login failed")); // or maybe some LoginFailedException() you can reuse
}
})