I wrote a RxJava implementation of a TokenManager for a remote API (that I'm consuming via Retrofit). However I ran into a snag where a method call with blockingGet() is resulting in skipped UI frames even though I subscribeOn(Schedulers.io())
Basically, I've included getToken() as a parameter in the API search() call method. If the token exists, it will be provided, if not, it will be fetched via the API token() call. <-- This is the problem. When this method gets called, it's resulting in skipped frames in the UI (progressbar freezes momentarily + respective logcat Choreographer skipped frames!! message)
Looking for suggestions on how to remedy the skipped frames, or suggestions on how to better implement this code.
ListFetcher (class that calls TokenManager getToken() )
public Single<List<Business>> getList(final String latitude, final String longitude) {
return api
.search(
tokenManager.getToken(), // <-- Here's the TokenManager reference
AppSettings.SEARCH_TERM,
latitude,
longitude,
AppSettings.SEARCH_RADIUS,
Yelp3Api.SEARCH_LIMIT)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(searchResponse -> {
if (searchResponse.getBusinesses().size() < searchResponse.getTotal()) {
return subsequentSearchCalls(searchResponse, latitude, longitude)
.map(businesses -> {
List<Business> list = new ArrayList<>();
list.addAll(searchResponse.getBusinesses());
list.addAll(businesses);
return list;
});
} else {
return Single.just(searchResponse.getBusinesses());
}
});
}
TokenManager getToken()
public synchronized String getToken() {
final String cachedToken = sharedPrefs.getString(tokenKey, "null");
if (cachedToken.equals("null")) {
String tokenString = api
.token(
Yelp3Api.GrantType.CLIENT_CREDENTIALS,
BuildConfig.YELPFUSION_CLIENT_ID,
BuildConfig.YELPFUSION_CLIENT_SECRET)
.subscribeOn(Schedulers.io())
.doOnSuccess(this::setSharedPrefToken)
.map(tokenResponse -> String.format(AUTH_FORMAT, tokenResponse.getAccessToken()))
.blockingGet();
return tokenString;
} else {
return String.format(AUTH_FORMAT, cachedToken);
}
}
Yelp3Api (Retrofit interface)
#FormUrlEncoded
#POST("oauth2/token")
Single<TokenResponse> token(
#Field("grant_type") String grantType,
#Field("client_id") String clientId,
#Field("client_secret") String clientSecret
);
#GET("v3/businesses/search")
Single<SearchResponse> search(
#Header("Authorization") String authorization,
#Query("term") String term,
#Query("latitude") String latitude,
#Query("longitude") String longitude,
#Query("radius") int radius,
#Query("limit") int limit
);
When you call getToken() it is not executed lazily. It executes blocking call on your UI thread.
To fix it, wrap getToken in Single.fromCallable and flatMap it above api.search()
Hope this clears out what is actually happening
I ended up re-writing this using okhttp authenticator to request auth credentials and an okhttp interceptor to add the auth header to outgoing requests.
According to the internet, this seems to be a better implementation.
However I'm accepting #Tuby's answer as it is a more direct resolution to the original question.
Related
I am sucessfully running local cloud function from postman with this url:
http://127.0.0.1:5001/spotnik-7de37/us-central1/getUsers
But on my app using:
val functions = Firebase.functions
functions.useEmulator("10.0.2.2", 5001)
functions.getHttpsCallable("http://127.0.0.1:5001/spotnik-7de37/us-central1/getUsers")
I get:
Failed com.google.firebase.functions.FirebaseFunctionsException: INTERNAL
Take a look at Call functions from your app. Just use the name of the function for the getHttpsCallable() call:
private Task<String> addMessage(String text) {
// Create the arguments to the callable function.
Map<String, Object> data = new HashMap<>();
data.put("text", text);
data.put("push", true);
return mFunctions
.getHttpsCallable("addMessage")
.call(data)
.continueWith(new Continuation<HttpsCallableResult, String>() {
#Override
public String then(#NonNull Task<HttpsCallableResult> task) throws Exception {
// This continuation runs on either success or failure, but if the task
// has failed then getResult() will throw an Exception which will be
// propagated down.
String result = (String) task.getResult().getData();
return result;
}
});
}
In your case I might try the above example using:
httpsCallable('spotnik-7de37')
Or use this:
getHttpsCallableFromUrl("http://127.0.0.1:5001/spotnik-7de37/us-central1/getUsers")
Also ensure you are using the proper IP for your emulator:
functions.useEmulator("127.0.0.1", 5001);
Additionally, if you want to use the entire URL string for the call, you can use getHttpsCallableFromUrl.
Need to use HTTPS protocol and not HTTP. See https://firebase.google.com/docs/emulator-suite/connect_functions
Instrument your app for HTTPS functions emulation Each HTTPS function
in your code will be served from the local emulator using the
following URL format:
http://$HOST:$PORT/$PROJECT/$REGION/$NAME
For example a simple helloWorld function with the default host port
and region would be served at:
https://localhost:5001/$PROJECT/us-central1/helloWorld
I am trying to get the CompanyEndpoint for each client's site but I am confused with the use of retrofit on the interface.
Here's what I have so far:
CompanyName : "company1"
CompanyEndpoint : "https://example.com"
IdentityEndpoint : "https://example.com/identity"
AppLoginMode : "Anonymous"
AppRouterApi.java
public interface AppRouterApi {
#GET("api/sites/{CompanyName}")
Call<Company> getCompanyName (#Url String companyName);
}
Company.java
public class Company {
String Endpoint;
public String getEndpoint() {
return endpoint;
}
}
MainActivity.java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
appRouterApi = retrofit.create(AppRouterApi.class);
getCompany();
}
private void getCompany(){
retrofit2.Call<Company> companyRequest = appRouterApi.getCompanyName(); //Error here saying a string cant be applied to ()
companyRequest.enqueue(new retrofit2.Callback<Company>() {
#Override
public void onResponse(retrofit2.Call<Company> call, retrofit2.Response<Company> response) {
if(!response.isSuccessful()){
textViewResult.setText("Code:" + response.code());
return;
}
Company company = response.body();
String content = "";
content += "Url" + company.getEndpoint();
textViewResult.setText(content);
}
#Override
public void onFailure(retrofit2.Call<Company> call, Throwable t) {
}
});
}
https://example/sites/{companyName}
So if I search for:
https://example/sites/company1
The JSON will have one object and I need to get the endpoint URL value which would be: https://company1.com
Edit: My textViewReslt is returning 403
There are several things going on as far as I can tell. Let me break it into chunks.
First thing is you're confusing the annotation #Path with the annotation #Url. They serve different purposes.
You use #Path when you want to format a bit of the path into the url inside the annotations like #GET.
public interface AppRouterApi {
#GET("api/sites/{CompanyName}")
Call<Company> getCompanyName (#Path("CompanyName") String companyName);
}
This interface will format the argument passed to getCompanyName as part of the path. Calling getCompanyName("foo") will call the endpoint "https://example.com/api/sites/foo".
You use #Url when you want to simply call that url. In this case, you only annotate the interface method with the http method. For example,
public interface AppRouterApi {
#GET
Call<Company> getCompanyName (#Url String url);
}
You then would have to call the method with the entire url. To call the same url as before you'd have to call getCompanyName("https://example.com/api/sites/foo").
This is the main difference of usage between these 2 annotations. The reason why you're seeing null in your text view is because you're model's attribute name doesn't match the json. You have 2 options.
First, you can change the model to:
public class Company {
String CompanyEndpoint;
public String getEndpoint() {
return endpoint;
}
}
CompanyEndpoint is the exact same name as you have in the json. Another approach, is to tell your json serializer what name you want to use. Since you're using gson, you can use #SerializedName like so:
public class Company {
#SerializedName("CompanyEndpoint")
String Endpoint;
public String getEndpoint() {
return endpoint;
}
}
#SerializedName("CompanyEndpoint") tells gson which name to use while serializing and deserializing.
In essence, you have 2 options. You either use the endpoint, or the company's name. If you don't expect the domain to change, I'd suggest using the first approach with the #Path annotation. This is what it's usually done with Retrofit and personally, I think it's easier to handle than passing urls around. My suggestion is, use a model like:
public class Company {
#SerializedName("CompanyName")
String name;
public String getName() {
return name;
}
}
This would let you access the company's name property and call getCompanyName(company.getName()). Retrofit would format the company's name into the path and you'd call the right url.
I'm trying to build my URL using Retrofit 2.0. The problem is it's returning this URL:
http://query.yahooapis.com/v1/public/yql?&q=select%20*%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22YHOO%22)&format=json%26diagnostics%3Dtrue%26env%3Dstore%253A%252F%252Fdatatables.org%252Falltableswithkeys%26callback%3D
I want it to return this URL instead:
https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22YHOO%22)&format=json&diagnostics=true&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=
Can anyone please advise how do I fix this?
Here is the code that returns the URL:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
mQuoteAdapter = new QuoteAdapter(items);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.question_list);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(mQuoteAdapter);
StockApiServiceInterface stockApiServiceInterface = retrofit.create(StockApiServiceInterface.class);
stockApiServiceInterface.listQuotes(
"select * from yahoo.finance.quotes where symbol in (\"YHOO\")",
"json&diagnostics=true&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=")
.enqueue(new Callback<ResultWrapper>() {
#Override
public void onResponse(Response<ResultWrapper> response) {
response.body().getQuery().getResults().getQuote().getAsk();
}
#Override
public void onFailure(Throwable t) {
Log.e("listQuotes threw: ", t.getMessage());
}
});
Here is my StockApiService:
public final class StockApiService {
public interface StockApiServiceInterface {
#GET("v1/public/yql?")
Call<ResultWrapper> listQuotes(
#Query("q") String query,
#Query("format") String env
);
}
}
Remove the question-mark form your request URL like this:
#GET("v1/public/yql")
And seperate the parameters you are sending here:
"json&diagnostics=true&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=")
Into query items.
Your method should look something like this:
#Query("q") String query,
#Query("format") String format,
#Query("diagnostics") boolean diagnostics,
#Query("env") String enviroment,
#Query("callback") boolean callback
Slight change from Ian that simplifies it a little:
public final class StockApiService {
public interface StockApiServiceInterface {
#GET("v1/public/yql?format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys")
Call<ResultWrapper> listQuotes(
#Query("q") String query,
#Query("diagnostics") boolean diagostics
);
}
}
Unchanging query strings parameters can be included in the method annotation and retrofit should merge them together. Also, I removed the callback parameter because that is a thing for websites called JSONP and isn't relevant to an Android app.
The actual problem you have is that you are giving Retrofit a precomposed partial query string and asking it to encode it for you. Retrofit doesn't know that it's a precomposed query string, so it does what it's supposed to: treats it as the value of a query string parameter and URL encodes it. #Ian is absolutely right that you need to split them up.
I have a new Android project I am working on, and Retrofit2 is working well for me. However, I have one url that I need to hit, with one of two strings, on top of the data I send.
Right now it looks like this:
#FormUrlEncoded
#POST("token")
Call<AccessResponse> requestAccess(#Field("grant_type") String type, #Field("code") String authCode, #Field("client_id") String ApiKey);
the grant type is only one of two things, and I would like to abstract it away, into static urls, like this:
#FormUrlEncoded
#POST("token")
#Field("grant_type","type1")
Call<AccessResponse> requestAccess( #Field("code") String authCode, #Field("client_id") String ApiKey);
#FormUrlEncoded
#POST("token")
#Field("grant_type","type2")
Call<AccessResponse> refreshAccess( #Field("code") String authCode, #Field("client_id") String ApiKey);
Is there a way to accomplish this? my 2 days of google-fu haven't worked for me, nor has browsing the API docs and code. I just don't want to have to keep track of the correct string in the various places in my code.
Turns out the answer is "You can't right now".
There is an open issue on Github for the feature request
There is an alternative approach, a FieldMap object with a method example as mentioned in this SO post, but it is not exactly what I was looking for, and overkill for just one field.
Could the Retrofit RequestInterceptor do the job ?
It can inject parameters into each request... so from there you could maybe write a method that injects the right parameter depending on what you're trying to do...
RequestInterceptor requestInterceptor = new RequestInterceptor() {
#Override
public void intercept(RequestFacade request) {
request.addQueryParam("grant_type", getGrantType());
}
};
private String getGrantType()
{
// do your stuff and :
return "type1"; // or "type2"
}
My problem is i can't get infinite stream with Retrofit. After i get credentials for initial poll() request - i do initial poll() request. Each poll() request responds in 25 sec if there is no change, or earlier if there are any changes - returning changed_data[]. Each response contains timestamp data needed for next poll request - i should do new poll() request after each poll() response. Here is my code:
getServerApi().getLongPollServer()
.flatMap(longPollServer -> getLongPollServerApi(longPollServer.getServer()).poll("a_check", Config.LONG_POLLING_SERVER_TIMEOUT, 2, longPollServer.getKey(), longPollServer.getTs(), "")
.take(1)
.flatMap(longPollEnvelope -> getLongPollServerApi(longPollServer.getServer()).poll("a_check", Config.LONG_POLLING_SERVER_TIMEOUT, 2, longPollServer.getKey(), longPollEnvelope.getTs(), "")))
.retry()
.subscribe(longPollEnvelope1 -> {
processUpdates(longPollEnvelope1.getUpdates());
});
I'm new to RxJava, maybe i don't understand something, but i can't get infinite stream. I get 3 calls, then onNext and onComplete.
P.S. Maybe there is a better solution to implement long-polling on Android?
Whilst not ideal, I believe that you could use RX's side effects to achieve a desired result ('doOn' operations).
Observable<CredentialsWithTimestamp> credentialsProvider = Observable.just(new CredentialsWithTimestamp("credentials", 1434873025320L)); // replace with your implementation
Observable<ServerResponse> o = credentialsProvider.flatMap(credentialsWithTimestamp -> {
// side effect variable
AtomicLong timestamp = new AtomicLong(credentialsWithTimestamp.timestamp); // computational steering (inc. initial value)
return Observable.just(credentialsWithTimestamp.credentials) // same credentials are reused for each request - if invalid / onError, the later retry() will be called for new credentials
.flatMap(credentials -> api.query("request", credentials, timestamp.get())) // this will use the value from previous doOnNext
.doOnNext(serverResponse -> timestamp.set(serverResponse.getTimestamp()))
.repeat();
})
.retry()
.share();
private static class CredentialsWithTimestamp {
public final String credentials;
public final long timestamp; // I assume this is necessary for you from the first request
public CredentialsWithTimestamp(String credentials, long timestamp) {
this.credentials = credentials;
this.timestamp = timestamp;
}
}
When subscribing to 'o' the internal observable will repeat. Should there be an error then 'o' will retry and re-request from the credentials stream.
In your example, computational steering is achieved by updating the timestamp variable, which is necessary for the next request.