Retrofit and SimpleXML for unknown root element? - android

We are currently working on setting up Retrofit for an XML API - unfortunately each request can return a response with one of two different root elements.
Normally, each response looks like this (of course, the actual elements contained within the <response> tag vary with each request):
<?xml version="1.0" encoding="UTF-8"?>
<response>
<SomeInfo>Some Info</SomeInfo>
<MoreInfo>More Info</MoreInfo>
</response>
And each error looks like this (the structure here is the same for each response):
<?xml version="1.0" encoding="UTF-8"?>
<error>
<code>125002</code>
<message></message>
</error>
Now, the only way we've found so far to get this work in a somewhat generic way is the following:
public interface Api {
#GET("/api/sessionToken")
Observable<ResponseBody> requestSessionToken();
#GET("/api/pinStatus")
Observable<ResponseBody> requestPinStatus();
}
public class RestClient {
public RestClient() {
// ...
mApiService = retrofit.create(Api.class);
}
public Observable<PinStatusResponse> requestPinStatus() {
return mApiService.requestPinStatus()
.flatMap(foo(PinStatusResponse.class, PinStatusResponseData.class));
}
public Observable<SessionTokenResponse> requestSessionToken() {
return mApiService.requestSessionToken()
.flatMap(foo(SessionTokenResponse.class, SessionTokenResponseData.class));
}
private final <O extends MyResponse, I> Func1<ResponseBody, Observable<T>> foo(final Class<O> outerCls, final Class<I> innerCls) {
return new Func1<ResponseBody, Observable<O>>() {
#Override
public Observable<O> call(ResponseBody responseBody) {
try {
final String xmlString = responseBody.string();
final XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(new ByteArrayInputStream(xmlString.getBytes(Charset.forName("UTF-8"))), null);
parser.nextTag();
final String rootTag = parser.getName();
final Serializer serializer = new Persister();
if (TextUtils.equals(rootTag, "error")) {
final MyError myError = serializer.read(MyError.class, xmlString);
return Observable.just((O) outerCls.getConstructor(MyError.class, innerCls).newInstance(myError, null));
} else if (TextUtils.equals(rootTag, "response")) {
final I data = serializer.read(innerCls, xmlString);
return Observable.just((T) outerCls.getConstructor(MyError.class, innerCls).newInstance(null, data));
}
} catch (XmlPullParserException e) {
return Observable.error(e);
} catch (IOException e) {
return Observable.error(e);
} catch (Exception e) {
return Observable.error(e);
}
return Observable.error(new Exception("Should not be reached..."));
}
};
}
​}
Where the Response classes look like this:
public abstract class MyResponse<T> {
public final MyError error;
public final T data;
protected MyResponse(MyError error, T data) {
this.error = error;
this.data = data;
}
}
and:
public final class PinStatusResponse extends MyResponse<PinStatusResponseData> {
public PinStatusResponse(MyError error, PinStatusResponseData data) {
super(error, data);
}
}
And all the *Data classes correspond directly to the (non-error) XML responses.
Now, my question is: Is there really no easier way to solve this? (And if so, is this a sign of bad API design?).

This is what the #ElementUnion annotation is for. You could probably work it out using pure Retrofit+SimpleXML API by using an annotated object like so:
#Root
public class ResponseBody {
public interface IApiResponse {
}
#Root
public static class ValidResponse implements IApiResponse {
#Element(name="SomeInfo") String someInfo;
#Element(name="MoreInfo") String moreInfo;
}
#Root
public static class ErrorResponse implements IApiResponse {
#Element(name="code") int code;
#Element(name="message") String message;
}
#ElementUnion({
#Element(name="response", type=ValidResponse.class),
#Element(name="error", type=ErrorResponse.class)
})
IApiResponse apiResponse;
}
* Concrete structure of 'ValidResponse' and 'ErrorReponse' would have to be altered according to the real XML structure you have. You might also wish to consider adding 'strict=false' in their #Root.
As for your 'Api' interface, it would then have to look like this (note I've slipped in the usage of Retrofit's Call class):
public interface Api {
#GET("/api/sessionToken")
Call<ResponseBody> requestSessionToken();
#GET("/api/pinStatus")
Call<ResponseBody> requestPinStatus();
}
And finally, the call itself (to e.g. requestPinStatus()) should be worked out according to this skeletal implementation:
Call<ResponseBody> result = mApiService.requestPinStatus();
result.enqueue(new Callback<ResponseBody>() {
#Override
public void onResponse(Response<ResponseBody> response, Retrofit retrofit) {
// ...
if (response.body().apiResponse instanceof ValidResponse) {
// ...
} else if (response.body().apiResponse instanceof ErrorResponse) {
// ...
}
}
#Override
public void onFailure(Throwable t) {
// ...
}
});
For more info on ElementUnion and Simple-XML annotations, refer to this guide.

Related

How to handle errors from API in Login Activity (Template from Android Studio)

I'm implementing a simple login to an endpoints using Retrofit2. Things work fine when the user credentials are correct but break when I try to enter a non valid data.
I'm trying to handle the errors when the user is not found but I can't find a way to do that.
The error response looks like:
{
"0": [
"erreur",
"statut"
],
"erreur": "Erreur, connexion echoue.",
"statut": "KO"
}
This response has status 200 despite being an error.
The app is crashing with NPE in the LoginRepository where I'm trying to save user's data to SharedPreferences because the error result is not handled so the app threat any response as Successful.
The sample provides a Result class which doesn't seem to work for my use case because the response is always successful:
public class Result<T> {
// hide the private constructor to limit subclass types (Success, Error)
private Result() {
}
#Override
public String toString() {
if (this instanceof Result.Success) {
Result.Success success = (Result.Success) this;
return "Success[data=" + success.getData().toString() + "]";
} else if (this instanceof Result.Error) {
Result.Error error = (Result.Error) this;
return "Error[exception=" + error.getError().toString() + "]";
}
return "";
}
// Success sub-class
public final static class Success<T> extends Result {
private T data;
public Success(T data) {
this.data = data;
}
public T getData() {
return this.data;
}
}
// Error sub-class
public final static class Error extends Result {
private Exception error;
public Error(Exception error) {
this.error = error;
}
public Exception getError() {
return this.error;
}
}
}
And here is how I'm handling the login in the LoginRepository:
public Result<LoggedInUser> login(String username, String password) {
// handle login
Result<LoggedInUser> result = dataSource.login(username, password);
if (result instanceof Result.Success) {
setLoggedInUser(((Result.Success<LoggedInUser>) result).getData());
}
return result;
}
Note: I don't have access to the server. I use Gson as converter
The login activity sample I used can be found here
UPDATE:
Login successful with valid credentials:
Check this answer it will help you.
#POST("end_path")
Call<ResponseBody> LoginCall(
#Field("email") String user_id,
#Part("paassword") String language
);
Call<ResponseBody> call = Constant.service.LoginCall(
"email", "pass");
call.enqueue(new retrofit2.Callback<ResponseBody>() {
#Override
public void onResponse(Call<ResponseBody> call,
Response<ResponseBody> response) {
if (response.isSuccessful()) {
try {
String responseData = response.body().string();
JSONObject object = new JSONObject(responseData);
if(object.getString("statut").equalsIgnoreCase("success")){
LoggedInUser successData = new
Gson().fromJson(responseData, LoggedInUser.class);
}else{
showToast("Email password incorrect");//or show you want
this message.
}
} catch (IOException e) {
e.printStackTrace();
} catch (JsonSyntaxException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
} else {
showToast("something_went_wrong");
}
}
#Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
t.printStackTrace();
}
});

XMLStreamException: ParseError at [row,col]:[1,1] ; only whitespace content allowed before start tag and not [

I've been trying and searching but can't seem to find a solution. I have this xml content:
<ArrayOfstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<string>C201711241121.png</string>
<string>G201711241121.png</string>
<string>I201711241121.png</string>
<string>I201711241121.png</string>
</ArrayOfstring>
provided by a link.
I've added the INTERNET permission in Android Manifest:
<uses-permission android:name="android.permission.INTERNET"/>
The class for the data I tried to implement:
#Root
#NamespaceList({
#Namespace(reference="http://www.w3.org/2001/XMLSchema-instance", prefix="i"),
#Namespace(reference="http://schemas.microsoft.com/2003/10/Serialization/Arrays")
})
public class ArrayOfstring {
#ElementList
private List<String> string;
public void setString(List<String> string) {
this.string = string;
}
public List<String> getString(){
return string;
}
}
The interface for Retrofit:
public interface WebAPI {
#GET("api/values")
Call<ArrayOfstring> loadArrayOfstring();
}
The class with callbacks:
public class Controller implements Callback<ArrayOfstring> {
static final String BASE_URL = "http://xx.xxx.xxx.xx:xxxx/";
public void start() {
Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(SimpleXmlConverterFactory.create()).build();
WebAPI vogellaAPI = retrofit.create(WebAPI.class);
Call<ArrayOfstring> call = vogellaAPI.loadArrayOfstring();
call.enqueue(this);
}
#Override
public void onResponse(Call<ArrayOfstring> call, Response<ArrayOfstring> response) {
if (response.isSuccessful()) {
ArrayOfstring resp = response.body();
if (resp == null){
Log.v("resp","is null");
return;
}
// System.out.println("Channel title: " + rss.getChannelTitle());
Log.v("response",resp.getString().size()+" size");
for (String str: resp.getString()){
Log.v("response", str+" string");
}
} else {
//System.out.println(response.errorBody());
Log.v("response", "error "+response.code());
}
}
#Override
public void onFailure(Call<ArrayOfstring> call, Throwable t) {
t.printStackTrace();
}
And I start the Controller so that the get request would begin in my activity, like this:
Controller ctrl = new Controller();
ctrl.start();
But the only result there seems to be
W/System.err: java.lang.RuntimeException: javax.xml.stream.XMLStreamException: ParseError at [row,col]:[1,1]
W/System.err: Message: only whitespace content allowed before start tag and not [
The link formed should be http://xx.xxx.xxx.xx:xxxx/api/values/
I know, I am a little late to the party. In case, if it helps, I am answering the question.
You need to pass "Accept", "Application/xml" header in the request.
I was facing the same issue, this worked for me.

Error while trying to cache a HashSet using Android Room Library

I'm willing to try the new Room Library from Android and I met the below error:
Error:(19, 29) error: Cannot figure out how to save this field into
database. You can consider adding a type converter for it.
This error refers to the following class member:
private HashSet<String> fruits;
I have the following class:
#Entity(tableName = "SchoolLunches")
public class SchoolLunch {
#PrimaryKey(autoGenerate = true)
private int lunchId;
private boolean isFresh;
private boolean containsMeat;
private HashSet<String> fruits;
public int getLunchId() {
return lunchId;
}
public void setLunchId(int lunchId) {
this.lunchId = lunchId;
}
public boolean isFresh() {
return isFresh;
}
public void setFresh(boolean fresh) {
isFresh = fresh;
}
public boolean isContainsMeat() {
return containsMeat;
}
public void setContainsMeat(boolean containsMeat) {
this.containsMeat = containsMeat;
}
public HashSet<String> getFruits() {
return fruits;
}
public void setFruits(HashSet<String> fruits) {
this.fruits = fruits;
}
Also, there is a relative DAO class:
#Dao
public interface SchoolLunchDAO {
#Query("SELECT * FROM SchoolLunches")
List<SchoolLunch> getAll();
#Insert
void insertAll(SchoolLunch... schoolLunches);
#Query("DELETE FROM SchoolLunches")
void deleteAll();
}
Since I'm trying to be a very good developer, I wrote a unit test as follows:
#Test
public void singleEntityTest() {
HashSet<String> fruitSet = new HashSet<>();
fruitSet.add("Apple");
fruitSet.add("Orange");
SchoolLunch schoolLunch = new SchoolLunch();
schoolLunch.setContainsMeat(false);
schoolLunch.setFresh(true);
schoolLunch.setFruits(fruitSet);
schoolLunchDAO.insertAll(schoolLunch);
List<SchoolLunch> schoolLunches = schoolLunchDAO.getAll();
assertEquals(schoolLunches.size(), 1);
SchoolLunch extractedSchoolLunch = schoolLunches.get(0);
assertEquals(false, extractedSchoolLunch.isContainsMeat());
assertEquals(true, extractedSchoolLunch.isFresh());
assertEquals(2, extractedSchoolLunch.getFruits().size());
}
What should I do here?
What should I do here?
You could create a type converter, as suggested by the error message. Room does not know how to persist a HashSet<String>, or a Restaurant, or other arbitrary objects.
Step #1: Decide what basic type you want to convert your HashSet<String> into (e.g., a String)
Step #2: Write a class with public static type conversion methods, annotated with #TypeConverter, to do the conversion (e.g., HashSet<String> to String, String to HashSet<String>), in some safe fashion (e.g., use Gson, formatting your String as JSON)
Step #3: Add a #TypeConverters annotation to your RoomDatabase or other scope, to teach Room about your #TypeConverter methods
For example, here are a pair of type converter methods for converting a Set<String> to/from a regular String, using JSON as the format of the String.
#TypeConverter
public static String fromStringSet(Set<String> strings) {
if (strings==null) {
return(null);
}
StringWriter result=new StringWriter();
JsonWriter json=new JsonWriter(result);
try {
json.beginArray();
for (String s : strings) {
json.value(s);
}
json.endArray();
json.close();
}
catch (IOException e) {
Log.e(TAG, "Exception creating JSON", e);
}
return(result.toString());
}
#TypeConverter
public static Set<String> toStringSet(String strings) {
if (strings==null) {
return(null);
}
StringReader reader=new StringReader(strings);
JsonReader json=new JsonReader(reader);
HashSet<String> result=new HashSet<>();
try {
json.beginArray();
while (json.hasNext()) {
result.add(json.nextString());
}
json.endArray();
}
catch (IOException e) {
Log.e(TAG, "Exception parsing JSON", e);
}
return(result);
}
I created the following class and now it works. Thank you, CommonsWare!
public class Converters {
private static final String SEPARATOR = ",";
#TypeConverter
public static HashSet<String> fromString(String valueAsString) {
HashSet<String> hashSet = new HashSet<>();
if (valueAsString != null && !valueAsString.isEmpty()) {
String[] values = valueAsString.split(SEPARATOR);
hashSet.addAll(Arrays.asList(values));
}
return hashSet;
}
#TypeConverter
public static String hashSetToString(HashSet<String> hashSet) {
StringBuilder stringBuilder = new StringBuilder();
for (String currentElement : hashSet) {
stringBuilder.append(currentElement);
stringBuilder.append(SEPARATOR);
}
return stringBuilder.toString();
}
}

how to do a PUT using retrofit with a JSONObject that contains a JSONArray

I'm trying to shift over from volley to retrofit and I don't fully understand how to do a PUT with a JSONObject that contains a JSONArray.
The JSONObject body that I want to send to the server should look like this:
{
“account”: [
{“availability”: “offline”}
]}
here is my pojo
public class AvailabilityModel {
JSONObject account;
public AvailabilityModel(JSONObject account) {
this.account = account;
}
}
and my interface
public interface AvailabilityAPI {
#Headers( "Content-Type: application/json" )
#PUT(PATH)
Call<AccountParentModel> setAvailability(#Path("parameter") String accountId, #Body AvailabilityModel object);
class Factory {
private static AvailabilityAPI service;
public static AvailabilityAPI getInstance() {
if (service == null) {
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(App.BASE_URL)
.build();
return service = retrofit.create(AvailabilityAPI.class);
} else {
return service;
}
}
}
}
and finally, In my activity I do this:
JSONObject account = new JSONObject();
JSONArray array = new JSONArray();
JSONObject obj = new JSONObject();
try {
obj.put("availability", "offline");
} catch (JSONException e) {
e.printStackTrace();
}
array.put(obj);
try {
cloudNumber.put("account", array);
} catch (JSONException e) {
e.printStackTrace();
}
Log.e(this.getClass().getSimpleName(), "JSONObj sent to the server is: " + account);
AvailabilityModel availabilityModel = new AvailabilityModel(account);
AvailabilityAPI.Factory.getInstance().setAvailability(accountId, availabilityModel).enqueue(new Callback<AccountParentModel>() {
#Override
public void onResponse(Call<CloudNumberParentModel> call, Response<CloudNumberParentModel> response) {
Log.e("HomeActivity", "Success: availability = " + response.body().cloudNumbers.get(0).getAvailability());
}
#Override
public void onFailure(Call<CloudNumberParentModel> call, Throwable t) {
Log.e(this.getClass().getSimpleName(), " No good bro " + t.getMessage());
}
});
the problem with this is the server receives it in this format:
{“nameValuePairs”:{“account”:{“values”:[{“nameValuePairs”:{“availability”:“available”}}]}}}
Any help will be much appreciated.
Dont do like that..
let me give some brief to simply understand you about that.
step 1. take your json reqest .
In your case
{ “account”: [ {“availability”: “offline”} ]}
Step 2 . make model class.
That i describe at here Link
So in your case your model class is.
public class AvailabilityModel {
private List<AccountBean> account;
public List<AccountBean> getAccount() {
return account;
}
public void setAccount(List<AccountBean> account) {
this.account = account;
}
public static class AccountBean {
/**
* availability : offline
*/
private String availability;
public String getAvailability() {
return availability;
}
public void setAvailability(String availability) {
this.availability = availability;
}
}
}
Step 3 : putting value inside model class
first
AccountBean account = new AccountBean();
account.setAvailability("offline");
now take one array
List<AccountBean>list = new List<AccountBean>();
list.add(account);
so above is your list of account. now one step to complete make model.
AvailabilityModel model =new AvailabilityModel();
model. setAccount(list);
Happy coding :)

Retrofit and Centralized Error Handling

Each request to the server may return error_code. I want to handle these error in one place
when I was using AsyncTask I had a BaseAsyncTask like that
public abstract class BaseAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
protected Context context;
private ProgressDialog progressDialog;
private Result result;
protected BaseAsyncTask(Context context, ProgressDialog progressDialog) {
this.context = context;
this.progressDialog = progressDialog;
}
#Override
protected void onPreExecute() {
super.onPreExecute();
}
#Override
protected void onPostExecute(Result result) {
super.onPostExecute(result);
HttpResponse<ErrorResponse> response = (HttpResponse<ErrorResponse>) result;
if(response.getData().getErrorCode() != -1) {
handleErrors(response.getData());
}else
onResult(result);
}
private void handleErrors(ErrorResponse errorResponse) {
}
public abstract void onResult(Result result);
}
But, using retrofit each request has its error handling callback:
git.getFeed(user,new Callback<gitmodel>() {
#Override
public void success(gitmodel gitmodel, Response response) {
}
#Override
public void failure(RetrofitError error) {
}
});
}
});
How can I handle all errors in one place?
If you need to get some 'logic' error, then you need some Java logic since it's not a Retrofit feature so basically:
Create a Your implementation Callback that implements the Retrofit Callback
Create a base object that define the method 'isError'
Modify Retrofit RestAdapter in order to get your Callback instead of the Retrofit One
MyCallback.java
import android.util.Log;
import retrofit.Callback;
import retrofit.client.Response;
public abstract class MyCallback<T extends MyObject> implements Callback<T> {
#Override
public final void success(T o, Response response) {
if (o.isError()) {
// [..do something with error]
handleLogicError(o);
}
else {
handleSuccess(o, response);
}
}
abstract void handleSuccess(T o, Response response);
void handleLogicError(T o) {
Log.v("TAG", "Error because userId is " + o.id);
}
}
MyObject.java (the base class for all your objects you get from Retrofit)
public class MyObject {
public long id;
public boolean isError() {
return id == 1;
}
}
MyRealObject.java - a class that extends the base object
public class MyRealObject extends MyObject {
public long userId;
public String title;
public String body;
}
RetroInterface.java - the interface used by retrofit you should be familiar with
import retrofit.http.GET;
import retrofit.http.Path;
public interface RetroInterface {
#GET("/posts/{id}")
void sendGet(#Path("id") int id, MyCallback<MyRealObject> callback);
}
And finally the piece of code where you use all the logic
RestAdapter adapter = new RestAdapter.Builder()
.setEndpoint("http://jsonplaceholder.typicode.com")
.build();
RetroInterface itf = adapter.create(RetroInterface.class);
itf.sendGet(2, new MyCallback<MyRealObject>() {
#Override
void handleSuccess(MyRealObject o, Response response) {
Log.v("TAG", "success");
}
#Override
public void failure(RetrofitError error) {
Log.v("TAG", "failure");
}
});
If you copy and paste this code, you'll get an error when you'll execute the itf.sendGet(1, new MyCallback..) and a success for itf.sendGet(2, new MyCallback...)
Not sure I understood it correctly, but you could create one Callback and pass it as a parameter to all of your requests.
Instead of:
git.getFeed(user,new Callback<gitmodel>() {
#Override
public void success(gitmodel gitmodel, Response response) {
}
#Override
public void failure(RetrofitError error) {
}
});
First define your Callback:
Callback<gitmodel> mCallback = new Callback<gitmodel>() {
#Override
public void success(gitmodel gitmodel, Response response) {
}
#Override
public void failure(RetrofitError error) {
// logic to handle error for all requests
}
};
Then:
git.getFeed(user, mCallback);
In Retrofit you can specify ErrorHandler to all requests.
public class ApiErrorHandler implements ErrorHandler {
#Override
public Throwable handleError(RetrofitError cause) {
//here place your logic for all errors
return cause;
}
}
Apply it to RestAdapter
RestAdapter.Builder()
.setClient(client)
.setEndpoint(endpoint)
.setErrorHandler(errorHandler)
.build();
I think that it is what you asked for.
In Retrofit2 you can't set an ErrorHandler with the method .setErrorHandler(), but you can create an interceptor to fork all possible errors centralised in one place of your application.
With this example you have one centralised place for your error handling with Retrofit2 and OkHttpClient. Just reuse the Retrofit object (retrofit).
You can try this standalone example with a custom interceptor for network and server errors. These both will be handled differently in Retrofit2, so you have to check the returned error code from the server over the response code (response.code()) and if the response was not successful (!response.isSuccessful()).
For the case that the user has no connection to the network or the server you have to catch an IOException of the method Response response = chain.proceed(chain.request()); and handle the network error in the catch block.
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
try {
Response response = chain.proceed(chain.request());
if (!response.isSuccessful()) {
Log.e("tag", "Failure central - response code: " + response.code());
Log.e("tag", "central server error handling");
// Central error handling for error responses here:
// e.g. 4XX and 5XX errors
switch (response.code()) {
case 401:
// do something when 401 Unauthorized happened
// e.g. delete credentials and forward to login screen
// ...
break;
case 403:
// do something when 403 Forbidden happened
// e.g. delete credentials and forward to login screen
// ...
break;
default:
Log.e("tag", "Log error or do something else with error code:" + response.code());
break;
}
}
return response;
} catch (IOException e) {
// Central error handling for network errors here:
// e.g. no connection to internet / to server
Log.e("tag", e.getMessage(), e);
Log.e("tag", "central network error handling");
throw e;
}
}
})
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://10.0.2.2:8000/api/v1/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
UserRepository backendRepository = retrofit.create(UserRepository.class);
backendRepository.getUser("userId123").enqueue(new Callback<UserModel>() {
#Override
public void onResponse(Call<UserModel> call, retrofit2.Response<UserModel> response) {
Log.d("tag", "onResponse");
if (!response.isSuccessful()) {
Log.e("tag", "onFailure local server error handling code:" + response.code());
} else {
// its all fine with the request
}
}
#Override
public void onFailure(Call<UserModel> call, Throwable t) {
Log.e("tag", "onFailure local network error handling");
Log.e("tag", t.getMessage(), t);
}
});
UserRepository example:
public interface UserRepository {
#GET("users/{userId}/")
Call<UserModel> getUser(#Path("userId") String userId);
}
UserModel example:
public class UserModel implements Parcelable {
#SerializedName("id")
#Expose
public String id = "";
#SerializedName("email")
#Expose
public String mail = "";
public UserModel() {
}
protected UserModel(Parcel in) {
id = in.readString();
mail = in.readString();
}
public static final Creator<UserModel> CREATOR = new Creator<UserModel>() {
#Override
public UserModel createFromParcel(Parcel in) {
return new UserModel(in);
}
#Override
public UserModel[] newArray(int size) {
return new UserModel[size];
}
};
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(mail);
}
}
Fairly simply Retrofit custom error handling example. Is set up so that you don't need to do much work in the 'failure' handler of a retrofit call to get the user-visible error message to show. Works on all endpoints. There's lots of exception handling as our server folks like to keep us on our toes by sending all kinds of random stuff..!
// on error the server sends JSON
/*
{ "error": { "data": { "message":"A thing went wrong" } } }
*/
// create model classes..
public class ErrorResponse {
Error error;
public static class Error {
Data data;
public static class Data {
String message;
}
}
}
//
/**
* Converts the complex error structure into a single string you can get with error.getLocalizedMessage() in Retrofit error handlers.
* Also deals with there being no network available
*
* Uses a few string IDs for user-visible error messages
*/
private static class CustomErrorHandler implements ErrorHandler {
private final Context ctx;
public CustomErrorHandler(Context ctx) {
this.ctx = ctx;
}
#Override
public Throwable handleError(RetrofitError cause) {
String errorDescription;
if (cause.isNetworkError()) {
errorDescription = ctx.getString(R.string.error_network);
} else {
if (cause.getResponse() == null) {
errorDescription = ctx.getString(R.string.error_no_response);
} else {
// Error message handling - return a simple error to Retrofit handlers..
try {
ErrorResponse errorResponse = (ErrorResponse) cause.getBodyAs(ErrorResponse.class);
errorDescription = errorResponse.error.data.message;
} catch (Exception ex) {
try {
errorDescription = ctx.getString(R.string.error_network_http_error, cause.getResponse().getStatus());
} catch (Exception ex2) {
Log.e(TAG, "handleError: " + ex2.getLocalizedMessage());
errorDescription = ctx.getString(R.string.error_unknown);
}
}
}
}
return new Exception(errorDescription);
}
}
// When creating the Server...
retrofit.RestAdapter restAdapter = new retrofit.RestAdapter.Builder()
.setEndpoint(apiUrl)
.setLogLevel(retrofit.RestAdapter.LogLevel.FULL)
.setErrorHandler(new CustomErrorHandler(ctx)) // use error handler..
.build();
server = restAdapter.create(Server.class);
// Now when calling server methods, get simple error out like this:
server.postSignIn(login,new Callback<HomePageResponse>(){
#Override
public void success(HomePageResponse homePageResponse,Response response){
// Do success things!
}
#Override
public void failure(RetrofitError error){
error.getLocalizedMessage(); // <-- this is the message to show to user.
}
});

Categories

Resources