Skip to content

Instantly share code, notes, and snippets.

@OleksandrKucherenko
Last active March 23, 2018 06:20
Show Gist options
  • Save OleksandrKucherenko/c573573e50403811f42e to your computer and use it in GitHub Desktop.
Save OleksandrKucherenko/c573573e50403811f42e to your computer and use it in GitHub Desktop.
Volley Library adaptation for Loader pattern.
import android.os.Handler;
import android.os.Message;
import android.support.v4.content.Loader;
import com.android.volley.NoConnectionError;
import com.android.volley.Response;
import com.android.volley.RetryPolicy;
import com.android.volley.TimeoutError;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonRequest;
import com.artfulbits.annotations.NonNull;
import com.artfulbits.io.InternetUtils;
import com.artfulbits.utils.LogEx;
import com.artfulbits.utils.ValidUtils;
import com.artfulbits.workout.TheApp;
import com.artfulbits.workout.utils.VolleyUtils;
import com.artfulbits.workout.web.handlers.AncestorsFactory;
import com.artfulbits.workout.web.responses.Ancestor;
import com.artfulbits.workout.web.responses.Error;
import org.json.JSONObject;
import java.util.logging.Logger;
/**
* Basic loader that converts server output to Ancestor business object.
*
* @param <T> the type of result Business Object, POJO class.
*/
public class JsonLoader<T extends Ancestor>
extends Loader<Ancestor>
implements Response.ErrorListener, Response.Listener<JSONObject>, Handler.Callback, WebApiRequest.Converter {
/* [ CONSTANTS ] ================================================================================================= */
/** Our own class Logger instance. */
private final static Logger _log = LogEx.getLogger(JsonLoader.class);
/** Message ID used for response results delivery to UI thread. */
private static final int MSG_QUERY_DONE = -1;
/** indicate normal state of the response. */
public static final int STATE_OK = 0;
/** indicate error state of the response. */
public static final int STATE_ERROR = -1;
/* [ MEMBERS ] =================================================================================================== */
/** Request to the server side. */
private JsonRequest<JSONObject> mRequest;
/** Main thread attached handler. Used for delivering server response to the UI thread. */
private final Handler mHandler = new Handler(this);
/** Reference on Business Objects Layer Factory, used for parsing/converting JSON to BO. */
private final AncestorsFactory mParserFactory;
/** Reference on parsed results. */
private Ancestor mParsedResults;
/* [ CONSTRUCTORS ] ============================================================================================== */
public JsonLoader(@NonNull final AncestorsFactory factory) {
super(TheApp.context());
ValidUtils.isNull(factory, "Instance of BOL Factory required.");
mParserFactory = factory;
}
/* [ GETTER / SETTER METHODS ] =================================================================================== */
public Builder requestBuilder() {
return new Builder();
}
/* package */ T getLastResult() {
return (T) mParsedResults;
}
/* package */ JsonRequest<JSONObject> getRequest() {
return mRequest;
}
/* package */ void setRequest(final JsonRequest<JSONObject> request) {
ValidUtils.isNull(request, "Expected instance of the JsonRequest class.");
mRequest = request;
}
/* [ Interface Handler.Callback ] ================================================================================ */
/** {@inheritDoc} */
@Override
public boolean handleMessage(final Message msg) {
// WARNING: deliver Result should be always called from UI thread
if (MSG_QUERY_DONE == msg.what) {
deliverResult((Ancestor) msg.obj);
return true;
}
return false;
}
/* [ Interface Response.ErrorListener ] ========================================================================== */
/** {@inheritDoc} */
@Override
public void onErrorResponse(final VolleyError error) {
// dump error message to Logs
_log.severe(LogEx.dump(error));
// this is often mean a network type error.
final Message message = mHandler.obtainMessage(MSG_QUERY_DONE, STATE_ERROR, STATE_ERROR, new Error(error));
// request execution in UI thread
mHandler.sendMessage(message);
if (error instanceof NoConnectionError || error instanceof TimeoutError) {
_log.severe("No Internet Connection? loader: " + hashCode());
// notify application about connectivity issues
InternetUtils.forceCheck(TheApp.context());
}
}
/* [ Interface WebApiRequest.Converter ] ========================================================================= */
/** {@inheritDoc} */
@Override
public void onConvertJson(final JSONObject response) {
ValidUtils.isUiThread("Data Processing should stay in background thread.");
_log.info("onResponse processing for url: " + mRequest.getOriginUrl());
Ancestor parsed = null;
try {
parsed = parseJson(response);
} catch (final Throwable ignored) {
_log.severe(LogEx.dump(ignored));
parsed = new Error(ignored);
}
// parsing error, method should always return instance of Ancestor
mParsedResults = ((null == parsed) ? new Error() : parsed);
}
/* [ Interface Response.Listener ] =============================================================================== */
/** {@inheritDoc} */
@Override
public void onResponse(final JSONObject response) {
final int state = (mParsedResults instanceof Error) ? STATE_ERROR : STATE_OK;
final Message message = mHandler.obtainMessage(MSG_QUERY_DONE, state, state, mParsedResults);
// request execution in UI thread
mHandler.sendMessage(message);
_log.info("Done - successful response. loader: " + hashCode());
}
/* [ IMPLEMENTATION & HELPERS ] ================================================================================== */
/** {@inheritDoc} */
@Override
protected void onForceLoad() {
super.onForceLoad();
ValidUtils.isNull(mRequest, "Expected instance of the JsonRequest class.");
// recover web request state from CANCELED via reflection (better to modify library of course)
try {
VolleyUtils.resetCancelState(mRequest);
} catch (final Throwable ignored) {
_log.severe(LogEx.dump(ignored));
}
_log.info("[loader] scheduled web request: " + mRequest.getUrl());
// add request into execution queue
TheApp.forUI().add(mRequest);
}
/** {@inheritDoc} */
@Override
protected void onReset() {
super.onReset();
onStopLoading();
// do cleanup
mParsedResults = null;
}
/* {@inheritDoc} */
@Override
protected void onAbandon() {
// in case of started loader, we have to request stop of all background things
if (isStarted()) {
stopLoading();
}
super.onAbandon();
}
/** {@inheritDoc} */
@Override
protected void onStartLoading() {
super.onStartLoading();
// deliver result if we already have it
if (null != mParsedResults) {
deliverResult(mParsedResults);
}
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
if (takeContentChanged() || null == mParsedResults) {
forceLoad();
}
}
/** {@inheritDoc} */
@Override
protected void onStopLoading() {
super.onStopLoading();
// WARNING: cancel state of the Request cannot be reset. we should override implementation for making
// possible state recovery. Cancel execution of the request.
mRequest.cancel();
}
/** Do background parsing of the server response. */
private Ancestor parseJson(final JSONObject response) throws Exception {
ValidUtils.isUiThread("Json Parsing should stay in background thread.");
return mParserFactory.parse(response);
}
/* [ NESTED DECLARATIONS ] ======================================================================================= */
/** Simple builder, that hides Web request creation. */
public final class Builder {
private String mUrl;
private int mHttpMethod;
private RetryPolicy mRetryPolicy;
private String mTag;
private boolean mShouldCache;
public Builder setUrl(final String url) {
mUrl = url;
return this;
}
public Builder setHttpMethod(final int type) {
mHttpMethod = type;
return this;
}
public Builder setRetryPolicy(final RetryPolicy policy) {
mRetryPolicy = policy;
return this;
}
public Builder setTag(final String tag) {
mTag = tag;
return this;
}
public Builder setShouldCache(final boolean cache) {
mShouldCache = cache;
return this;
}
/** Compose Web API request, configure it and assign to current loader. */
public void build() {
final WebApiRequest<T> request = new WebApiRequest<>(mHttpMethod, mUrl, JsonLoader.this);
if (null != mRetryPolicy) {
request.setRetryPolicy(mRetryPolicy);
}
if (null != mTag) {
request.setTag(mTag);
}
request.setShouldCache(mShouldCache);
// configure owner by newly created request
setRequest(request);
}
}
}
# --------------------------------------------------------------------------------------
# Volley Library
# --------------------------------------------------------------------------------------
-keep class com.android.volley.** { *; }
-keep interface com.android.volley.** { *; }
-dontwarn com.android.volley.**
package com.artfulbits.utils;
import com.artfulbits.annotations.NonNull;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/** Common reflection methods. */
public final class ReflectionUtils {
/* [ CONSTRUCTORS ] ============================================================================================== */
/** hidden constructor. */
private ReflectionUtils() {
throw new AssertionError();
}
/* [ STATIC METHODS ] ============================================================================================ */
/**
* Extract all fields of the provided class type.
*
* @param type the class type
* @return the list of found fields.
*/
public static List<Field> allFields(@NonNull final Class<?> type) {
final List<Field> fields = new LinkedList<Field>();
return allFields(fields, type);
}
/**
* Extract all fields in recursive manner.
*
* @param fields the extracted fields
* @param type the type to examine
* @return the extracted fields
*/
private static List<Field> allFields(@NonNull final List<Field> fields, @NonNull final Class<?> type) {
fields.addAll(Arrays.asList(type.getDeclaredFields()));
if (type.getSuperclass() != null) {
allFields(fields, type.getSuperclass());
}
return fields;
}
/**
* Find field by name in list of fields.
*
* @param fields the list of fields to process
* @param name the name to find.
* @return found field instance, otherwise {@code null}.
*/
public static Field find(@NonNull final List<Field> fields, @NonNull final String name) {
for (Field f : fields) {
if (name.equals(f.getName())) {
return f;
}
}
return null;
}
}
import com.android.volley.Request;
import com.artfulbits.annotations.NonNull;
import java.lang.reflect.Field;
import static com.artfulbits.utils.ReflectionUtils.allFields;
import static com.artfulbits.utils.ReflectionUtils.find;
/** Volley library hacking utils. */
public final class VolleyUtils {
/* [ CONSTANTS ] ================================================================================================= */
/** multi-threading access lock. */
private final static Object sLock = new Object();
/* [ STATIC MEMBERS ] ============================================================================================ */
/** Cached value. */
private static Field sCanceledField;
/* [ STATIC METHODS ] ============================================================================================ */
/**
* Reset cancel state of the provided instance.
*
* @param <T> the type parameter
* @param request the request instance
* @throws IllegalAccessException the illegal access exception
*/
public static <T> void resetCancelState(@NonNull final Request<T> request) throws IllegalAccessException {
if (request.isCanceled()) {
getCanceledField().setBoolean(request, false);
}
}
/**
* Gets canceled field by reflection and cache it for future calls. Reflection is a time consuming operation.
*
* @return the canceled field instance.
*/
private static Field getCanceledField() {
if (null == sCanceledField) {
synchronized (sLock) {
if (null == sCanceledField) {
final Field field = find(allFields(Request.class), "mCanceled");
field.setAccessible(true);
sCanceledField = field;
}
}
}
return sCanceledField;
}
}
import com.android.volley.NetworkResponse;
import com.android.volley.Response;
import com.android.volley.toolbox.JsonObjectRequest;
import com.artfulbits.annotations.NonNull;
import com.artfulbits.annotations.Nullable;
import org.json.JSONObject;
/**
* JSON Object request, deeply integrated with JsonLoader class.
*
* @see <a href="http://tiku.io/questions/4747168/android-volley-does-not-work-offline-with-cache">Offline Cache</a>
*/
/* package */ class WebApiRequest<T extends Ancestor> extends JsonObjectRequest {
/* [ MEMBERS ] =================================================================================================== */
/** Reference on data processing routine. */
private Converter mConverter;
/** Reference on owner of this request. */
private final JsonLoader<T> mOwner;
/* [ CONSTRUCTORS ] ============================================================================================== */
public WebApiRequest(final int method, final String url, @NonNull final JsonLoader<T> owner) {
super(method, url, null, owner /*Listener<JSONObject>*/, owner /*ErrorListener*/);
mOwner = owner;
// Say Volley that JSON response should be cached
setShouldCache(true);
// register data parsing
setDataConverter(owner /*DataProcessingListener*/);
}
/* [ GETTER / SETTER METHODS ] =================================================================================== */
/**
* Gets loader with easy type definition over generics.
*
* @return the loader instance.
*/
@NonNull
public JsonLoader<T> getLoader() {
return mOwner;
}
/**
* Sets data processing converter.
*
* @param converter the converter instance
* @return this instance
*/
@NonNull
public WebApiRequest setDataConverter(@Nullable final Converter converter) {
mConverter = converter;
return this;
}
/* [ IMPLEMENTATION & HELPERS ] ================================================================================== */
/** {@inheritDoc} */
@Override
protected Response<JSONObject> parseNetworkResponse(@NonNull final NetworkResponse response) {
// NOTE: excellent place for adding Cache header modifiers, Allows to override
// server cache policy in easy way.
final Response<JSONObject> result = super.parseNetworkResponse(response);
// if we have something for parsing, then forward it to 'callback'
if (null != mConverter && result.isSuccess()) {
mConverter.onConvertJson(result.result);
}
return result;
}
/* [ NESTED DECLARATIONS ] ======================================================================================= */
/** Callback interface that give a chance to convert JSON to Business Object and store it. */
public interface Converter {
/** Parse server side response. */
void onConvertJson(final JSONObject response);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment