Last active
March 23, 2018 06:20
-
-
Save OleksandrKucherenko/c573573e50403811f42e to your computer and use it in GitHub Desktop.
Volley Library adaptation for Loader pattern.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -------------------------------------------------------------------------------------- | |
# Volley Library | |
# -------------------------------------------------------------------------------------- | |
-keep class com.android.volley.** { *; } | |
-keep interface com.android.volley.** { *; } | |
-dontwarn com.android.volley.** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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