I'm totally new to RxJava and I've spent all day understanding it, I'm tying to think how to solve this problem:
I have one object, fetched by Retrofit, it contains two ArrayLists, I have to process every ArrayList differently. Currently it looks like:
apiService.getUser(token).enqueue(new Callback<User>() {
#Override
public void onResponse(Response<User> response) {
final User user = response.body();
for (Skill s : user.getSkills()) {
// process here first ArrayList
}
for (OrganizerAction o : user.getOrganizerActions()) {
// process here second ArrayList
}
}
#Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
});
UPDATE:
public class User {
// fields
#SerializedName("organizer_actions")
#Expose
private List<OrganizerAction> mOrganizerActions;
#SerializedName("skills")
#Expose
private List<Skill> mSkills;
public List<OrganizerAction> getOrganizerActions() {
return mOrganizerActions;
}
public List<Skill> getSkills() {
return mSkills;
}
}
Thanks,
Anton
This answer is for Retrofit 2.0.0-beta, which is what you appear to be using. Also, you didn't give your POJO or service definitions, so going to use a general GitHub API example as a guide, modify to match your specify data.
First step is to convert your service definition to use Observable instead of Call.
public interface GitHubService {
#GET("/users/{user}")
Observable<User> getUser(#Path("user") String user);
}
Where User is
public class User {
public String login;
public int id;
}
Next, add a custom call adapter with to your retrofit builder with addCallAdapterFactory --
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
Get your service in the usual way --
GitHubService gitHubService = retrofit.create(GitHubService.class);
Next, get your observable and call cache on it to create an Observable that will replay the result. From that Observable, you can subscribe multiple times, in your case, you can subscribe twice. Once for each type of data you are interested in, and use the map function to transform from the User object to your specific fields. map allows you to apply function to the data in the observable. See the docs for more details. In this example, we will make two streams. One each for the id and login fields.
Observable<User> getUserResult = gitHubService.getUser("octocat").cache(1);
getUserResult.map(new Func1<User, Integer>() {
#Override
public Integer call(User user) {
return user.id;
}
}).subscribe(new Action1<Integer>() {
#Override
public void call(Integer id) {
Log.d("Stream 1", "id = " + id);
}
});
getUserResult.map(new Func1<User, String>() {
#Override
public String call(User user) {
return user.login;
}
}).subscribe(new Action1<String>() {
#Override
public void call(String login) {
Log.d("Stream 2", "login = " + login);
}
});
Finally, make sure your gradle file has the needed dependencies,
compile 'io.reactivex:rxjava:1.0.14'
compile 'com.squareup.retrofit:retrofit:2.0.0-beta1'
compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta1'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1'
And, not directly related to your question, but if you are going to doing RxJava in Android, I recommend you checkout Retrolambda if you have not already. The above map and subscribe code, and Rx code in general, is more succinct with lambdas.
getUserResult.map(user -> user.id).subscribe(
id -> { Log.d("Stream 1", "id = " + id); }
);
getUserResult.map(user -> user.login).subscribe(
login -> { Log.d("Stream 2", "login = " + login); }
);
Related
So, I'm trying to connect to Scryfall's API and do an autocomplete call. I've been able to use their other call properly but this one I think where I'm having issue.
Here is the call: https://api.scryfall.com/cards/autocomplete?q=fire
q is the query and it will return a list of up to 20 items that could be auto-completed with the word 'fire'.
{
"object":"catalog",
"total_values":20,
"data": [
"Fire // Ice","Fire Imp","Firefly","Fire Whip","Fire Ants","Firebolt","Fireball","Fire Drake","Fire Snake","Firespout","Firestorm","Fireblast","Fire-Field Ogre","Fire Urchin","Fire Bowman","Fire Dragon","Fire at Will","Fire Ambush","Firemaw Kavu","Fire Juggler"
]
}
I am using retrofit2 for android.
Here is some of my code.
This is my interface for the endpoints
public interface ScryfallEndPoints {
//https://api.scryfall.com/cards/named?fuzzy=
#GET("cards/named")
Call<Card> getCard(
#Query(value=("fuzzy")) String name);
//https://api.scryfall.com/cards/autocomplete?q=
#GET("cards/autocomplete")
Call<Card> getCardAutoComplete(
#Query(value=("q")) String name);
}
This is a method I use in my activity to perform the call.
private void loadCardList()
{
final ScryfallEndPoints apiService =
APIClient.getClient().create(ScryfallEndPoints.class);
Call<Map<String, String>> call = apiService.getCardAutoComplete(str);
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
call.enqueue(new Callback<Map<String, String>>()
{
#Override
public void onResponse(Call<Map<String, String>> call, Response<Map<String, String>> response)
{
Toast.makeText(SuggestionResults.this, "onResponse()", Toast.LENGTH_SHORT).show();
}
#Override
public void onFailure(Call<Map<String, String>> call, Throwable t)
{
Toast.makeText(SuggestionResults.this, "onFailure()", Toast.LENGTH_SHORT).show();
}
});
//tv.setText(str);
}
Here is a method that is part of my model class.
#SerializedName("data")
private Map<String, String> cardList;
public Map<String, String> getCardList() {return cardList;}
So, I feel like there is definitely something maybe wrong in the way I am trying to access he data in my model class and maybe with the way I have it set up in my interface. When I make the call, it doesn't fail so I don't have error logs to show, i just know that it is going to the onFailure() method and I'm not sure why. I mostly need to figure this out then I can work on getting the list to populate. Also, if there is a way that I can see more of what is going on with the calls that I am making, that would be great too. Thanks!
#GET("cards/autocomplete")
Call<Card> getCardAutoComplete(
#Query(value=("q")) String name);
}
Then the calling part
//Call<Map<String, String>> call = apiService.getCardAutoComplete(str);
//It returns Call of Card type, hence it will be as follows
Call<Card> call = apiService.getCardAutoComplete(str);
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
call.enqueue(new Callback<Card>()
{
#Override
public void onResponse(Call<Card> call, Response<Card> response)
{
Toast.makeText(SuggestionResults.this, "onResponse()", Toast.LENGTH_SHORT).show();
}
#Override
public void onFailure(Call<Card> call, Throwable t)
{
Toast.makeText(SuggestionResults.this, "onFailure()", Toast.LENGTH_SHORT).show();
}
});
I'm just trying to learn mvvm, and I faced some issue - My List which should contain response from API are empty. I'm not sure why it is. Here's some code:
MainActivity
mViewModel = new ViewModelProvider(this).get(ViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Model>() {
#Override
public void onChanged(List<Model> list) {
if (data.size() > 0) {
data.clear();
}
if (list != null) {
data.addAll(list);
Log.i(TAG, "onChanged: " + data.size());
}
}
});
ViewModel
private Repository mRepository;
private MutableLiveData<List<Model> liveData;
public ViewModel(#NonNull Application application) {
super(application);
mRepository = Repository.getInstance();
liveData = mRepository.getData();
}
public MutableLiveData<List<Model> getData(){
return liveData;
}
Repository
public MutableLiveData<List<Model>> getData(){
MutableLiveData<List<Model> mLiveData = new MutableLiveData<>();
mApiCall.callApi()
.enqueue(new Callback<List<Model>() {
#Override
public void onResponse(Call<List<Model> call, Response<List<Model> response) {
mLiveData.setValue(response.body());
}
#Override
public void onFailure(Call<List<Model> call, Throwable t) {
t.getMessage();
}
});
return mLiveData;
}
in function getData() you are returning a live data and in this line in your view model :
liveData = mRepository.getData();
you are assigning it to liveDate which is a mutable live data created in your view model and the problem is here.
when this assignment is happened the observer in the liveData variable in view model will be removed and that's why we should use switchMap like this :
private var result : LiveData<List<Response>> = MutableLiveData()
result : LiveData<Response> = Transformations.map(mRepository.getData()){
it
}
and now all you need to do is to observe on repo in your view like this :
viewmodel.result.observe(this, Observer{ list ->
// to do with the result
})
response.body() will provide with class APIResponse. But what you need as response is List.
To get the expected response as List, try response.body().getMetadata().getResults()
It could be either repository initialisation as well. Do try creating
public void init() {
mRepository = Repository.getInstance();
liveData = mRepository.getData();
}
use viewModel.init();
Model class properties should match with response JSON fields to get the model objects from the response.
Debug on the response.body() and see the data coming. if the model is empty then there would be a mismatch on the fields.
UPDATE:::
I've updated the question to include demo other LiveData that were also required:
so we have userLD that we need the value of to get the goalWeeklyLD, and we need the goalWeeklyLD value to get the remaining 4 LiveData values as they come from Room querys that use goalWeekly.dateproperties in the query
:::::
I've hit a problem where I have a fragment that has to populate LiveData that uses a query dependent on another LiveData value.
how can i get my live data to work correctly when it is dependent on other results?
Without using The Transitions.map() the view model throws an error because the values of the other live data are still null.
with the Transitions.map() in the view model the activities observer throws an error because the LiveData is still null.
I could possibly cheat my way past this by using a horrendously big nested query to return all i need in one custom DTO. but i'd rather understand whats going on here and how to handle this sort of situation properly.
Hopefully some code will make this clear
The Activity:
public class SomeFragment extends Fragment {
public static SomeFragment newInstance() {
return new SomeFragment();
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
someViewModel = ViewModelProviders.of(this).get(SomeViewModel.class);
//getting user details from previous activity
Intent intent = getActivity().getIntent();
if (intent != null){
if (intent.hasExtra(USER_ID)){
user = new User(intent.getStringExtra(USERNAME));
user.setId(intent.getLongExtra(USER_ID,0));
someViewModel.setUserLD(user);
}
}
someViewModel.getUserLD().observe(this, new Observer<User>() {
#Override
public void onChanged(#Nullable User userVal) {
user = userVal;
}
});
someViewModel.getGoalWeeklyLD().observe(this, new Observer<User>() {
#Override
public void onChanged(#Nullable User userVal) {
user = userVal;
}
});
//the below Observer calls throw an error because LiveData is null. makes sense.
//but how can i say "don't try and observe these until the transition.map has ran (because then it wont be null after if my understanding is right)" or something to that effect
someViewModel.getFirstLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgSportGradeVal) {
//Update UI
}
});
someViewModel.getSecondLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
someViewModel.getThriLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
someViewModel.getFourthLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
}}
The View Model:
public class SomeViewModel extends AndroidViewModel {
DaoRepository daoRepository;
MutableLiveData<User> userLD;
LiveData<XObject> firstLD;
LiveData<XObject> secondLD;
LiveData<XObject> thirdLD;
LiveData<XObject> fourthLD;
public MutableLiveData<User> getUserLD() {
return userLD;
}
public void setUserLD(User user){
userLD.setValue(user);
}
public LiveData<XObject> getFirstLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getSecondLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getThirdLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getForthLD(long userId) {
return goalWeeklyLD;
}
public SomeViewModel(#NonNull Application application) {
super(application);
daoRepository = new DaoRepository(application);
userLD = new MutableLiveData<>();
//so the first LiveData waits for the user to be populated before getting its LiveData becasue we need the userId for our Room query to run
firstLD = Transformations.map(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()).getValue());
//the remaining live data uses values from the first...
setupOtherTransformMaps(userLD.getValue())
}
public void setupOtherTransformMaps(long userId) {
//the secondLD, thirdLD and fourthLD all depends on values from the first (in runs a query that uses its dateExpired)
secondLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
thirdLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
fourthLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
}}
Thankfully Google was smart and created a component which lets you combine variable number of LiveData into a single LiveData, and only emit events when you choose to do so!
This is called MediatorLiveData.
In your case though, you only need to channel 1 LiveData (userLD) into 1 another LiveData, that will emit each time userLd has a new value.
So you can use a predefined MediatorLiveData that does exactly this, specifically Transformations.switchMap.
firstLD = Transformations.switchMap(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()));
EDIT: Yup, you seem to need to expose these LiveData separately from one another, but they all depend on the first query to execute.
So you need to replace Transformations.map { ...getValue() with Transformations.switchMap and you'll be good to go.
public SomeViewModel(#NonNull Application application) {
super(application);
CustomApplication app = (CustomApplication) application;
daoRepository = app.daoRepository();
userLD = new MutableLiveData<>();
firstLD = Transformations.switchMap(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()));
secondLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
thirdLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
fourthLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
}
I am new to Reactor framework and trying to utilize it in one of our existing implementations. LocationProfileService and InventoryService both return a Mono and are to executed in parallel and have no dependency on each other (from the MainService). Within LocationProfileService - there are 4 queries issued and the last 2 queries have a dependency on the first query.
What is a better way to write this? I see the calls getting executed sequentially, while some of them should be executed in parallel. What is the right way to do it?
public class LocationProfileService {
static final Cache<String, String> customerIdCache //define Cache
#Override
public Mono<LocationProfileInfo> getProfileInfoByLocationAndCustomer(String customerId, String location) {
//These 2 are not interdependent and can be executed immediately
Mono<String> customerAccountMono = getCustomerArNumber(customerId,location) LocationNumber).subscribeOn(Schedulers.parallel()).switchIfEmpty(Mono.error(new CustomerNotFoundException(location, customerId))).log();
Mono<LocationProfile> locationProfileMono = Mono.fromFuture(//location query).subscribeOn(Schedulers.parallel()).log();
//Should block be called, or is there a better way to do ?
String custAccount = customerAccountMono.block(); // This is needed to execute and the value from this is needed for the next 2 calls
Mono<Customer> customerMono = Mono.fromFuture(//query uses custAccount from earlier step).subscribeOn(Schedulers.parallel()).log();
Mono<Result<LocationPricing>> locationPricingMono = Mono.fromFuture(//query uses custAccount from earlier step).subscribeOn(Schedulers.parallel()).log();
return Mono.zip(locationProfileMono,customerMono,locationPricingMono).flatMap(tuple -> {
LocationProfileInfo locationProfileInfo = new LocationProfileInfo();
//populate values from tuple
return Mono.just(locationProfileInfo);
});
}
private Mono<String> getCustomerAccount(String conversationId, String customerId, String location) {
return CacheMono.lookup((Map)customerIdCache.asMap(),customerId).onCacheMissResume(Mono.fromFuture(//query).subscribeOn(Schedulers.parallel()).map(x -> x.getAccountNumber()));
}
}
public class InventoryService {
#Override
public Mono<InventoryInfo> getInventoryInfo(String inventoryId) {
Mono<Inventory> inventoryMono = Mono.fromFuture(//inventory query).subscribeOn(Schedulers.parallel()).log();
Mono<List<InventorySale>> isMono = Mono.fromFuture(//inventory sale query).subscribeOn(Schedulers.parallel()).log();
return Mono.zip(inventoryMono,isMono).flatMap(tuple -> {
InventoryInfo inventoryInfo = new InventoryInfo();
//populate value from tuple
return Mono.just(inventoryInfo);
});
}
}
public class MainService {
#Autowired
LocationProfileService locationProfileService;
#Autowired
InventoryService inventoryService
public void mainService(String customerId, String location, String inventoryId) {
Mono<LocationProfileInfo> locationProfileMono = locationProfileService.getProfileInfoByLocationAndCustomer(....);
Mono<InventoryInfo> inventoryMono = inventoryService.getInventoryInfo(....);
//is using block fine or is there a better way to do?
Mono.zip(locationProfileMono,inventoryMono).subscribeOn(Schedulers.parallel()).block();
}
}
You don't need to block in order to get the pass that parameter your code is very close to the solution. I wrote the code using the class names that you provided. Just replace all the Mono.just(....) with the call to the correct service.
public Mono<LocationProfileInfo> getProfileInfoByLocationAndCustomer(String customerId, String location) {
Mono<String> customerAccountMono = Mono.just("customerAccount");
Mono<LocationProfile> locationProfileMono = Mono.just(new LocationProfile());
return Mono.zip(customerAccountMono, locationProfileMono)
.flatMap(tuple -> {
Mono<Customer> customerMono = Mono.just(new Customer(tuple.getT1()));
Mono<Result<LocationPricing>> result = Mono.just(new Result<LocationPricing>());
Mono<LocationProfile> locationProfile = Mono.just(tuple.getT2());
return Mono.zip(customerMono, result, locationProfile);
})
.map(LocationProfileInfo::new)
;
}
public static class LocationProfileInfo {
public LocationProfileInfo(Tuple3<Customer, Result<LocationPricing>, LocationProfile> tuple){
//do wathever
}
}
public static class LocationProfile {}
private static class Customer {
public Customer(String cutomerAccount) {
}
}
private static class Result<T> {}
private static class LocationPricing {}
Pleas remember that the first zip is not necessary. I re write it to mach your solution. But I would solve the problem a little bit differently. It would be clearer.
public Mono<LocationProfileInfo> getProfileInfoByLocationAndCustomer(String customerId, String location) {
return Mono.just("customerAccount") //call the service
.flatMap(customerAccount -> {
//declare the call to get the customer
Mono<Customer> customerMono = Mono.just(new Customer(customerAccount));
//declare the call to get the location pricing
Mono<Result<LocationPricing>> result = Mono.just(new Result<LocationPricing>());
//declare the call to get the location profile
Mono<LocationProfile> locationProfileMono = Mono.just(new LocationProfile());
//in the zip call all the services actually are executed
return Mono.zip(customerMono, result, locationProfileMono);
})
.map(LocationProfileInfo::new)
;
}
I'm new in RxJava so I still have many doubts.
I'm creating this:
#Override
public Single<Result> saveUser(final User user) {
return Single.create(new Single.OnSubscribe<Result>() {
#Override
public void call(final SingleSubscriber<? super Result> singleSubscriber) {
if(user.isValid()){
save(user);
//Set result ok
singleSubscriber.onSuccess(result);
} else {
//This sets an error.
singleSubscriber.onError(error);
}
}
});
}
Depending of the success of the operation, the Single emits it's data and who is subscribed receives the data.
The problem now is that at some point I need to store a list of users. Something like:
public void saveUsers(List<User> listOfUsers){
for (User user : listOfUsers) {
saveUser(user);
}
}
How can I create an Single so I can be subscribed to the initial Single
I would rather create flatmap out of observable list.
public void saveUsers(List<User> listOfUsers){
Observable.from(listOfUsers).flatMap((User user)->{
if(user.isValid()){
save(user);
//Set result ok
return Observable.just(result);
}else
return Observable.error(new RuntimeException("..."));
}).retry(2);
}
If you make your saveUsers method blocking, call Observable#toBlocking.