I'm using Retrofit with RxJava for the network calls and RxBinding for view operations. In signup screen, upon clicking 'Register' button I'm posting the info to the local server using the MyApi service.
SignupActivity.class
mCompositeSubscription.add(RxView.clicks(mRegisterButton).debounce(300, TimeUnit.MILLISECONDS).
subscribe(view -> {
registerUser();
}, e -> {
Timber.e(e, "RxView ");
onRegistrationFailed(e.getMessage());
}));
private void registerUser() {
mCompositeSubscription.add(api.registerUser(mEmail,
mPassword, mConfirmPassword)
.subscribe(user -> {
Timber.d("Received user object. Id: " + user.getUserId());
}, e -> {
Timber.e(e, "registerUser() ");
onRegistrationFailed(e.getMessage());
}));
}
MyApi.class
public Observable<User> registerUser(String username, String password, String confirmPassword) {
return mService.registerUser(username, password, confirmPassword)
.compose(applySchedulers());
}
#SuppressWarnings("unchecked") <T> Observable.Transformer<T, T> applySchedulers() {
return observable -> observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
MyService.class
#FormUrlEncoded #POST("users/")
Observable<User> registerUser(#Path("email") String username,
#Path("password") String password, #Path("password_confirmation") String confirmPassword);
The call fails with IllegalArgumentException since I'm posting an invalid info.
What's my main issue is, upon IllegalArgumentException I thought RxJava would execute registerUser()#ErrorHandler() since my registerUser service call failed with exception but instead it calls RxView#ErrorHandler().
How can I make/force registerUser()#ErrorHandler() to take care of the exception occurred during the network call?
My bad, the network call doesn't fail with IllegalArgumentException but the request construction itself failed with IllegalArgumentException.
java.lang.IllegalArgumentException: URL "users/" does not contain {email}".
Instead of using #Field annotation for constructing the POST body , I'd mistakenly used #Path annotation.
The correct definition:
#FormUrlEncoded #POST("users/")
Observable<User> registerUser(#Field("email") String username,
#Field("password") String password, #Field("password_confirmation") String confirmPassword);
Related
I integrated the witter login in android. But on response it always returning me in the logcat:
code=400, message=Bad Request
and
"data":"{\"+clicked_branch_link\":false,\"+is_first_session\":false}",
I am to see the token, secret value in logcat if I printed it. But response returning 400 always. I used here branch IO for deep linking.
public void attachTwitter(String token, String secret) {
apiService.attachTwitterAccount(PreferenceHandler.readString(EditProfileActivity.this, SIGNIN_ID, ""),
token, secret, "twitter").enqueue(new Callback<Object>() {
#Override
public void onResponse(Call<Object> call, Response<Object> response) {
Log.i("accessToken", "onResponse");
if (!response.isSuccessful()) {
try {
JSONObject object = new JSONObject(response.errorBody().string());
JSONObject error = object.optJSONObject("error");
String code = error.optString("code");
String description = error.optString("description");
if (code.equalsIgnoreCase("338")) {
showCustomDialog(EditProfileActivity.this, string(R.string.server_error_description_338));
switchTwitter.setChecked(false);
}
} catch (Exception e) {
}
}
}
#Override
public void onFailure(Call<Object> call, Throwable t) {
Log.i("accessToken", "onFailure");
switchTwitter.setChecked(false);
}
});
}
attachTwitterAccount methods code is:
#FormUrlEncoded
fun attachTwitterAccount(#Field("id") id: String,
#Field("authToken") token: String,
#Field("authSecret") authSecret: String,
#Field("typeAttach") typeAttach: String): Call<Any>
Can anyone please advise how I can fix this issue?
A Bad request means that the request that you are sending to the server is not in the way or form it is expecting. What do the docs say about that request? is it a Post? a Get?. If it is a POST then you should send a Body.
To send a body and a POST you first need to create a class for the body. In Kotlin it would be something like this:
class Body(var id: String,
var authToken: String,
var authSecret: String,
var accomplished: Double,
var typeAttach: String
)
Then you call the body in your request:
#POST("post value which")
fun attachTwitterAccount(#Body body:Body): Call<Any>
Finally you call it from your attach Twitter function. You create your Body instance and then pass it as argument.
If you are still getting the same error, check the Twitter docs, you might be missing something.
Its always a good idea to try that same call to the server in other enviroment (such as Postman) and see if it works there? this is a good way of determining if the problem is in the server side or in your application side.
Let me know if it works
I want to use rxjava and retrofit.
this is my retrofit builder:
Retrofit provideRetrofit(OkHttpClient okHttpClient) {
return new Retrofit.Builder()
.client(okHttpClient)
.baseUrl(UrlManager.API_HOST)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
}
and okHttpClient:
public OkHttpClient client(HttpLoggingInterceptor loggingInterceptor, Cache cache) {
return new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(loggingInterceptor)
.connectTimeout(10, TimeUnit.SECONDS)
.build();
}
In my Login Activity I insert my user and password and when loginButton is clicked this method in called:
public DisposableObserver observeLoginButton() {
return view.observeLoginBtn()
.subscribeOn(AndroidSchedulers.mainThread())
.doOnNext(new Consumer<Object>() {
#Override
public void accept(Object o) throws Exception {
view.loadButtonAnimation(); //load animation
}
})
.map(new Function<Object, String>() {
#Override
public String apply(Object o) throws Exception {
return view.getUserAndPassword(); // get userName and password as a string from edittext
}
})
.switchMap(new Function<String, Observable<String>>() {
#Override
public Observable<String> apply(String s) throws Exception {
String[] info = s.split(" "); // split user name and pass
return model.getLoginCookie(info[0],info[1]); // send userName and pass to model and call my service
}
})
.observeOn(Schedulers.io())
.subscribeWith(view.observer());
}
For test i have inserted myself login info to sending a service. this is my getLoginCookie method:
public Observable<String> getLoginCookie(String userName, String password) {
Map<String, Object> obj = new ArrayMap<>();
obj.put("username", "JuJzWgbsDJ0lUlFYVzoxWg");
obj.put("password", "DD0vCYmzJuIPff9iKUpfQA");
obj.put("customCredential", "6298f927-f98a-44eb-a312-780674a76245,Mobile89954324581380882887");
obj.put("isPersistent", false);
RequestBody body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
(new JSONObject(obj)).toString());
return service.getAuthentication(body);
}
My service work as a json parametr.So in order to i have used RequestBody to convert my map to json. Then i have called my service :
#POST(UrlManager.AUTHENTICATION+"Login")
Observable<String> getAuthentication(#Body RequestBody params);
When i run my app and click on login button i got this:
D/Payesh: --> POST http://****/Authentication.svc/json/Login (183-byte body)
W/System.err: remove failed: ENOENT (No such file or directory) : /data/data/com.groot.payesh/cache/okhttp_cache/journal.tmp
D/Payesh: <-- HTTP FAILED: android.os.NetworkOnMainThreadException
I/----->: apply: null
1- Why I got android.os.NetworkOnMainThreadException ?
2- How could i trace that my parameter is correct is send to my web service and does connect is established or not? I have just got java.lang.NullPointerException: The supplied value is null on onError(Throwable e) in observer.
Why I got android.os.NetworkOnMainThreadException ?
You are subscribing on AndroidSchedulers.mainThread() and observing on Schedulers.io(). That means you are doing network request on Main Thread. Switch Thread before network request by adding .observeOn(Schedulers.io()) before switchMap. And finally Observe On Main Thread if you are doing any view related task after finish.
return view.observeLoginBtn()
.subscribeOn(AndroidSchedulers.mainThread())
.doOnNext(new Consumer<Object>() {
#Override
public void accept(Object o) throws Exception {
view.loadButtonAnimation(); //load animation
}
})
.map(new Function<Object, String>() {
#Override
public String apply(Object o) throws Exception {
return view.getUserAndPassword(); // get userName and password as a string from edittext
}
})
.observeOn(Schedulers.io())
.switchMap(new Function<String, Observable<String>>() {
#Override
public Observable<String> apply(String s) throws Exception {
String[] info = s.split(" "); // split user name and pass
return model.getLoginCookie(info[0],info[1]); // send userName and pass to model and call my service
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(view.observer());
For
remove failed: ENOENT (No such file or directory) : /data/data/com.groot.payesh/cache/okhttp_cache/journal.tmp
make sure hoy have successfully created FIle for cache.
How could i trace that my parameter is correct is send to my web
service and does connect is established or not?
As you have used HttpLoggingInterceptor this will print all info about your request like Header, Body, Response.
I am using retrofit 2 as a REST client and I cannot figure out how to deserialise a dynamic JSON response.
Depending on the status value (success or failure), our JSON can have two different objects in the result field:
A successful response returns a User object:
{
"status": 200,
"message": "OK",
"result": {
"id": "1",
"email": "bla#bla.bla"
...
}
}
A failed response returns an Error object:
{
"status": 100,
"message": "FAILED",
"result": {
"error": "a user with this account email address already exists"
}
}
I have created 3 POJO classes...
APIResponse:
public class APIResponse<T> {
#Expose private int status;
#Expose private String message;
#Expose private T result;
...
}
User:
public class User {
#Expose private String id;
#Expose private String email;
...
}
Error:
public class Error {
#Expose private String error;
...
}
Here is how I make the call:
#FormUrlEncoded
#PUT(LOGIN)
Call<APIResponse<User>> login(#Field("email") String email, #Field("password") String password);
And here is how I get a response:
#Override
public void onResponse(Call<APIResponse<User>> call, Response<APIResponse<User>> response) {
...
}
#Override
public void onFailure(Call<APIResponse<User>> call, Throwable t) {
...
}
Question:
The API call expects a return type of Call<APIReponse<User>>. However, it might not get a User object back... So how do I modify this approach to accept either APIResponse<User> or APIResponse<Error>.
In other words, how do I deserialise JSON data that can be in two different formats?
Solutions I have looked at:
Including 'error' field in User class or extending Error class (ugly).
Custom interceptor or converter (struggled to understand).
Convince API devs to change it and make my life easier :)
I am not sure if the solution is viable, but you can try changing <APIResponse<User>> to <APIResponse<Object>>
Call<APIResponse<Object>> login(#Field("email") String email, #Field("password") String password);
#Override
public void onResponse(Call<APIResponse<Object>> call, Response<APIResponse<Object>> response) {
//depending on the response status, you can cast the object to appropriate class
Error e = (Error)response.body().getResult();
//or
User u = (User)response.body().getResult();
}
or another alternative, use String instead of POJO
Call<String> login(#Field("email") String email, #Field("password") String password);
retrieve the JSON and serialize manually or use GSON
You should check the response code and use the desired class to process the json
I'm using RxJava, Retrofit and MockWebServer I'm doing unit tests on my services.
My restAdapter have a custom error handler returning a custom throwable depending on the error code :
RestAdapter restAdapter = new RestAdapter.Builder().setErrorHandler(new RetrofitErrorHandler(context))
I tried to use a TestSubscriber and to subscribe like this:
TestSubscriber<X> testSubscriber = new TestSubscriber<>();
Observable<X> observable = mService.myCall(null, email);
observable.subscribe(testSubscriber);
but the .getOnErrorEvents() returns 0 event.
What should I do ?
Retrofit returns as a successful onNext event any kind of response, even if the status code differs from 2XX.
I propose to map the response and manually throw an error if the response.isSuccess() method returns false.
public Observable<UserEntity> loginUser(UserEntity user, String password) {
return this.restApi.doLogin(user.getEmail(), password)
.map(new Func1<Response<UserEntity>, UserEntity>() {
#Override
public UserEntity call(Response<UserEntity> userEntityResponse) {
if (!userEntityResponse.isSuccess()) throw new RuntimeException();
return userEntityResponse.body();
}
});
}
I have next interface declaration:
public interface FundaService
{
#GET( "/feeds/Aanbod.svc/json/{key}" )
Observable<JsonResponse> queryData( #Path( "key" ) String key, #Query("type" ) String type, #Query( "zo" ) String search, #Query( "page" ) int page, #Query( "pagesize" ) int pageSize );
}
That I use after with Retrofit. What would be an elegant way of testing that I didn't make mistakes in URL declaration and query parameters?
I see that I can mock web layer and check urls with parameters.
UPDATE
I modified it:
public interface FundaService
{
String KEY_PATH_PARAM = "key";
String FEED_PATH = "/feeds/Aanbod.svc/json/{" + KEY_PATH_PARAM + "}";
String TYPE_QUERY_PARAM = "type";
String SEARCH_QUERY_PARAM = "zo";
String PAGE_QUERY_PARAM = "page";
String PAGESIZE_QUERY_PARAM = "pagesize";
#GET( FEED_PATH )
Observable<JsonResponse> queryData( #Path( KEY_PATH_PARAM ) String key, #Query( TYPE_QUERY_PARAM ) String type,
#Query( SEARCH_QUERY_PARAM ) String search, #Query( PAGE_QUERY_PARAM ) int page,
#Query( PAGESIZE_QUERY_PARAM ) int pageSize );
}
And partially testing it, like:
public class FundaServiceTest
{
#Test
public void PathKeyIsCorrect()
throws Exception
{
assertThat( FundaService.KEY_PATH_PARAM ).isEqualTo( "key" );
}
#Test
public void FeedPathIsCorrect()
throws Exception
{
assertThat( FundaService.FEED_PATH ).isEqualTo( "/feeds/Aanbod.svc/json/{key}" );
}
}
You can use an okhttp interceptor to inspect the final request built by retrofit without using a mock http server. It gives you a chance to inspect the request a bit earlier. Suppose we want to test the following interface -
public interface AwesomeApi {
#GET("/cool/stuff")
Call<Void> getCoolStuff(#Query(("id"))String id);
}
The first test runs 'validateEagerly` to do a validation of the entire interface. Useful to have in case your other test cases don't touch all the interface methods. The second test is an example of how you might verify a specific call is generating the expected url.
public class AwesomeApiTest {
#Test
public void testValidInterface() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.example.com/")
.addConverterFactory(GsonConverterFactory.create())
// Will throw an exception if interface is not valid
.validateEagerly()
.build();
retrofit.create(AwesomeApi.class);
}
#Test(expected = NotImplementedException.class)
public void testCoolStuffRequest() throws Exception {
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new Interceptor() {
#Override
public com.squareup.okhttp.Response intercept(Chain chain) throws IOException {
final Request request = chain.request();
// Grab the request from the chain, and test away
assertEquals("HTTP methods should match", "GET", request.method());
HttpUrl url = request.httpUrl();
// Test First query parameter
assertEquals("first query paramter", "id", url.queryParameterName(0));
// Or, the whole url at once --
assertEquals("url ", "http://www.example.com/cool/stuff?id=123", url.toString());
// The following just ends the test with an expected exception.
// You could construct a valid Response and return that instead
// Do not return chain.proceed(), because then your unit test may become
// subject to the whims of the network
throw new NotImplementedException();
}
});
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
AwesomeApi awesomeApi = retrofit.create(AwesomeApi.class);
awesomeApi.getCoolStuff("123").execute();;
}
}
I got this idea from browsing retrofit's own tests. Other people's tests are often great inspiration!