JSON
{
"1":{"name":"Samsung","price":51200.6},
"2":{"name":"Lg","price":5400.6},
"3":{"name":"Alcatel","price":4500.6},
"4":{"name":"iPhone","price":4800.3},
"7":{"name":"iPad","price":2850.1}
}
Data
public class GoodsItem {
String name;
float price;
}
Loader
private String url = "http://192.168.1.103/shop.json";
private static final int LOADER_GOODS_ID = 1;
Map<Integer, GoodsItem> mGoodsMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
FeedLoader.with(this).addLoader(LOADER_GOODS_ID, url, HashMap.class, new DelivererFeedLoader.Listener<Map<Integer, GoodsItem>>() {
@Override
public void onResponse(int loaderId,
Map<Integer, GoodsItem> goodsMap) {
mGoodsMap = goodsMap;
for (Map.Entry<Integer, GoodsItem> entry : mGoodsMap.entrySet()) {
Log.d(TAG , "Goods item : " + entry.getKey() + " : " + entry.getValue());
}
}
@Override
public void onErrorResponse(VolleyError data) {
Log.d(TAG , "onErrorResponse :" + data);
}
}).start(LOADER_GOODS_ID, this);
Доступ к библиотеке предоставлен через простой Singleton. Здесь все просто, как в Picaso
public static FeedLoader with(Context context) {
if (singleton == null) {
synchronized (FeedLoader.class) {
if (singleton == null) {
singleton = new FeedLoader(context);
}
}
}
return singleton;
}
Добавление Типов и Листнера делается через addLoader:
loaderId — ID лоадера, просто число
url — Адрес feed
loaderClazz — Это тип класса, который хотим распарсить
DelivererFeedLoader.Listener — Callback через который вернутся данные или ошибка
public FeedLoader addLoader(int loaderId,
String url,
Class<?> loaderClazz,
DispatcherData.Listener callback){
dispatcherData.putLoaderClazz(loaderId, loaderClazz);
dispatcherData.putCallBack(loaderId, callback);
dispatcherData.putUrlFeed(loaderId, url);
dispatcherData.putUseCache(loaderId, false); // default
return singleton;
}
Запускаем Loader через start(...):
public void start(int loaderId, final FragmentActivity activity) {
assert activity instanceof FragmentActivity : "Run possible only from FragmentActivity";
Bundle bundle = new Bundle();
bundle.putParcelable("dispatcherData", dispatcherData);
activity.getSupportLoaderManager().
restartLoader(loaderId, bundle, callback);
}
}
Собственно четыре файла реализации Alfa
package net.appz.feedloader;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.util.Log;
import com.android.volley.Response;
/**
* Created by App-z.net on 04.04.15.
*/
public class FeedLoader {
private static volatile FeedLoader singleton = null;
private Context context = null;
private boolean DEBUG = true;
private String TAG = getClass().getSimpleName();
private DispatcherData dispatcherData = new DispatcherData();
public FeedLoader(Context context) {
if (context == null) {
throw new IllegalArgumentException("Context must not be null.");
}
this.context = context.getApplicationContext();
}
LoaderManager.LoaderCallbacks<Response<Object>> callback = new LoaderManager.LoaderCallbacks<Response<Object>>() {
@Override
public Loader<Response<Object>> onCreateLoader(int id, Bundle args) {
return new FeedLoaderWrapper(context , args);
}
@Override
public void onLoadFinished(Loader<Response<Object>> loader, Response<Object> data) {
if( data.isSuccess() )
dispatcherData.onResponse(loader.getId(), data.result);
else
dispatcherData.onErrorResponse(loader.getId(), data.error);
}
@Override
public void onLoaderReset(Loader<Response<Object>> loader) {
if( DEBUG ) Log.d(TAG, "onLoaderReset :" + loader.getId());
}
};
public FeedLoader addLoader(int loaderId,
String url,
Class<?> loaderClazz,
DispatcherData.Listener callback){
dispatcherData.putLoaderClazz(loaderId, loaderClazz);
dispatcherData.putCallBack(loaderId, callback);
dispatcherData.putUrlFeed(loaderId, url);
dispatcherData.putUseCache(loaderId, false); // default
return singleton;
}
public void start(int loaderId, final FragmentActivity activity) {
assert activity instanceof FragmentActivity : "Run possible only from FragmentActivity";
Bundle bundle = new Bundle();
bundle.putParcelable("dispatcherData", dispatcherData);
activity.getSupportLoaderManager().
restartLoader(loaderId, bundle, callback);
}
public void destroy(int loaderId, final FragmentActivity activity) {
assert activity instanceof FragmentActivity : "Run possible only from FragmentActivity";
activity.getSupportLoaderManager().destroyLoader(loaderId);
}
public static FeedLoader with(Context context) {
if (singleton == null) {
synchronized (FeedLoader.class) {
if (singleton == null) {
singleton = new FeedLoader(context);
}
}
}
return singleton;
}
public FeedLoader useCache(int loaderId) {
dispatcherData.putUseCache(loaderId, true);
return singleton;
}
public FeedLoader resetCache(int loaderId) {
dispatcherData.putUseCache(loaderId, false);
return singleton;
}
}
package net.appz.feedloader;
/**
* Created by App-z.net on 05.04.15.
*/
import android.content.Context;
import android.os.Bundle;
import android.support.v4.content.Loader;
import android.util.Log;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.Volley;
//http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html
/**
*
*
*
* @param <D>
*/
class FeedLoaderWrapper<D> extends Loader<Response<D>> {
private boolean DEBUG = true;
private String TAG = getClass().getSimpleName();
private DispatcherData dispatcherData;
private RequestQueue requestQueue;
private Response<D> mCachedResponse;
/**
* Stores away the application context associated with context.
* Since Loaders can be used across multiple activities it's dangerous to
* store the context directly; always use {@link #getContext()} to retrieve
* the Loader's Context, don't use the constructor argument directly.
* The Context returned by {@link #getContext} is safe to use across
* Activity instances.
*
* @param context used to retrieve the application context.
*/
public FeedLoaderWrapper(Context context, Bundle bundle) {
super(context);
dispatcherData = bundle.getParcelable("dispatcherData");
requestQueue = Volley.newRequestQueue(context);
// run only once
onContentChanged();
}
/**
* Get Data
*/
private void doRequest(Class<?> clazz) {
final boolean useCache = dispatcherData.getUseCache(getId());
if (DEBUG) Log.i(TAG, "useCache : " + getId() + " : " + useCache);
final String urlFeed = dispatcherData.getUrlFeed(getId());
if ( !useCache )
requestQueue.getCache().remove(urlFeed);
final GsonRequest gsonRequest = new GsonRequest(urlFeed,
clazz,
null, useCache,
new Response.Listener<D>() {
@Override
public void onResponse(D data) {
mCachedResponse = Response.success(data, null);
deliverResult(mCachedResponse);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError volleyError) {
mCachedResponse = Response.error(volleyError);
deliverResult(mCachedResponse);
}
});
requestQueue.add(gsonRequest);
}
@Override
public void deliverResult(Response<D> data) {
if (isReset()) {
// The Loader has been reset; ignore the result and invalidate the data.
//releaseResources(data);
if (DEBUG) Log.i(TAG, "Loader deliverResult() isReset()");
return;
}
// Hold a reference to the old data so it doesn't get garbage collected.
// We must protect it until the new data has been delivered.
Response<D> oldData = mCachedResponse;
mCachedResponse = data;
if (isStarted()) {
// If the Loader is in a started state, deliver the results to the
// client. The superclass method does this for us.
super.deliverResult(data);
if (DEBUG) Log.i(TAG, "Loader deliverResult() isStarted()");
}
if (DEBUG) Log.i(TAG, "Loader deliverResult()");
}
@Override
protected void onStartLoading() {
if (takeContentChanged())
forceLoad();
}
@Override
protected void onStopLoading() {
if (DEBUG) Log.i(TAG, "Loader onStopLoading()");
requestQueue.cancelAll(this);
super.onStopLoading();
}
@Override
protected void onReset() {
if (DEBUG) Log.i(TAG, "Loader onReset()");
requestQueue.cancelAll(this);
super.onReset();
}
@Override
public void onForceLoad() {
super.onForceLoad();
String urlFeed = dispatcherData.getUrlFeed(getId());
if (DEBUG) Log.d(TAG, "Loader onForceLoad() : feedUrl = " + urlFeed);
doRequest(dispatcherData.getLoaderClazz(getId()));
}
}
package net.appz.feedloader;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.volley.VolleyError;
import java.util.HashMap;
/**
* Created by App-z.net on 05.04.15.
*/
public class DispatcherData implements Parcelable {
private HashMap<Integer, Class> loaderClazzMap = new HashMap<>();
private HashMap<Integer, Listener> callBackMap = new HashMap<>();
private HashMap<Integer, String> urlFeeds = new HashMap<>();
private HashMap<Integer, Boolean> useCache = new HashMap<>();
public DispatcherData(){}
void putUseCache(int loaderId, boolean cache){
useCache.put(loaderId, cache);
}
boolean getUseCache(int loaderId){
return useCache.get(loaderId);
}
void putCallBack(int loaderId, Listener callback){
callBackMap.put(loaderId, callback);
}
//Listener getCallBack(int loaderId){
// return callBackMap.get(loaderId);
//}
void onResponse(int loaderId, Object data){
callBackMap.get(loaderId).onResponse(loaderId, data);
}
void onErrorResponse(int loaderId, VolleyError error){
callBackMap.get(loaderId).onErrorResponse(error);
}
String getUrlFeed(int loaderId){
return urlFeeds.get(loaderId);
}
void putUrlFeed(int loaderId, String url){
urlFeeds.put(loaderId, url);
}
Class getLoaderClazz(int loaderId){
return loaderClazzMap.get(loaderId);
}
void putLoaderClazz(int loaderId, Class loaderClazz){
loaderClazzMap.put(loaderId, loaderClazz);
}
protected DispatcherData(Parcel in) {
useCache = (HashMap) in.readValue(HashMap.class.getClassLoader());
loaderClazzMap = (HashMap) in.readValue(HashMap.class.getClassLoader());
callBackMap = (HashMap) in.readValue(HashMap.class.getClassLoader());
urlFeeds = (HashMap) in.readValue(HashMap.class.getClassLoader());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeValue(useCache);
dest.writeValue(loaderClazzMap);
dest.writeValue(callBackMap);
dest.writeValue(urlFeeds);
}
@SuppressWarnings("unused")
public static final Parcelable.Creator<DispatcherData> CREATOR = new Parcelable.Creator<DispatcherData>() {
@Override
public DispatcherData createFromParcel(Parcel in) {
return new DispatcherData(in);
}
@Override
public DispatcherData[] newArray(int size) {
return new DispatcherData[size];
}
};
public interface Listener<D>{
void onResponse(int loaderId, D data);
void onErrorResponse(VolleyError data);
}
}
package net.appz.feedloader;
import com.android.volley.AuthFailureError;
import com.android.volley.Cache;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.toolbox.HttpHeaderParser;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
/**
* Created by App-z.net on 29.03.15.
*/
public class GsonRequest<T> extends Request<T> {
private final Gson gson = new Gson();
private final Class<T> clazz;
private final Map<String, String> headers;
private final Response.Listener<T> listener;
private boolean useCache = false;
/**
* Make a GET request and return a parsed object from JSON.
*
* @param url URL of the request to make
* @param clazz Relevant class object, for Gson's reflection
* @param headers Map of request headers
*/
public GsonRequest(String url, Class<T> clazz, Map<String, String> headers, boolean useCache,
Response.Listener<T> listener, Response.ErrorListener errorListener ) {
super(Method.GET, url, errorListener);
this.clazz = clazz;
this.headers = headers;
this.listener = listener;
this.useCache = useCache;
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
return headers != null ? headers : super.getHeaders();
}
@Override
protected void deliverResponse(T response) {
listener.onResponse(response);
}
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
String json = new String(
response.data,
HttpHeaderParser.parseCharset(response.headers));
return Response.success(
gson.fromJson(json, clazz),
useCache ?
parseIgnoreCacheHeaders(response) :
HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
} catch (JsonSyntaxException e) {
return Response.error(new ParseError(e));
}
}
/**
* Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
* Cache-control headers are ignored. SoftTtl == 3 mins, ttl == 24 hours.
* @param response The network response to parse headers from
* @return a cache entry for the given response, or null if the response is not cacheable.
*/
public static Cache.Entry parseIgnoreCacheHeaders(NetworkResponse response) {
long now = System.currentTimeMillis();
Map<String, String> headers = response.headers;
long serverDate = 0;
String serverEtag = null;
String headerValue;
headerValue = headers.get("Date");
if (headerValue != null) {
serverDate = HttpHeaderParser.parseDateAsEpoch(headerValue);
}
serverEtag = headers.get("ETag");
final long cacheHitButRefreshed = 1 * 60 * 1000; // in 1 minutes cache will be hit, but also refreshed on background
final long cacheExpired = 24 * 60 * 60 * 1000; // in 24 hours this cache entry expires completely
final long softExpire = now + cacheHitButRefreshed;
final long ttl = now + cacheExpired;
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
entry.etag = serverEtag;
entry.softTtl = softExpire;
entry.ttl = ttl;
entry.serverDate = serverDate;
entry.responseHeaders = headers;
return entry;
}
}
Проект на GitHub
RoboSpice
- Пока нет проверок на ошибки добавляемых параметров в FeedLoader
- Добавлен кэш запросов
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (9)
withoutuniverse
06.04.2015 14:08+2В андроиде лоадеры перезапускаются в некоторых случаях автоматически, если это угодно юзеру. Используется ли такой подход в вашей реализации?
Код с with(context) некорректен, так как передав экземпляр Activity в него, она будет жить до смерти приложения.
Для примера — я нахожусь на Activity1, вызываю данный метод и происходит инициализация, затем закрываю Activity1 и открываю Activity2 и в этом случае синглтон все еще ссылается на уже убитый системой экземпляр. Чтобы предотвратить это, сделайте так, как действительно сделано в Picasso — используйте
this.context = context.getApplicationContext();
Ваш кодpublic static FeedLoader with(Context context) { if (singleton == null) { synchronized (FeedLoader.class) { if (singleton == null) { singleton = new FeedLoader(context); } } } return singleton; }
andreich
06.04.2015 14:15мне кажется что activity передается, чтобы в дальнейшем сделать вот это
((FragmentActivity)context).getSupportLoaderManager(). restartLoader(loaderId, bundle, callback);
withoutuniverse
06.04.2015 14:41+1В таком случае, это очень и очень плохой код. Это тоже самое, что в метод передавать Object, а затем его приводить к FragmentActivity даже без проверки типа объекта.
andreich
06.04.2015 15:07я согласен с вами абсолютно. мне кажется автору стоит немного подумать над реализацией и привести код в порядок.
app-z Автор
06.04.2015 21:15Согласен с вами! С проверкой и без преобразования
public void start(int loaderId, final FragmentActivity context) { .... assert context instanceof FragmentActivity : "Run possible only from FragmentActivity"; context.getSupportLoaderManager().restartLoader(loaderId, bundle, callback); }
app-z Автор
06.04.2015 21:38Вы правы! Первоначальная цель была посмотреть как получится, потом интересно ли вообще в использовании.
nekdenis
06.04.2015 17:24Если уж так хочется работать с сетью через Loaders, то есть скрещенный Retrofit+AsyncTaskLoader: github.com/rciovati/retrofit-loaders-example
Пользовался данным методом неоднократно.app-z Автор
06.04.2015 21:36Retrofit сам по себе умеет делать асинхронные запросы. Поэтому оборачивать его дополнительно в AsyncTaskLoader не требуется. Если конечно не использовать не асинхронный вызов.
Второе, на сколько понял из кода используется общий callback
public class MyActivity extends Activity implements Callback<List<Issue>> {
Или нет?
static class IssuesLoader extends RetrofitLoader<List<Issue>, GitHub> {
Сразу с ходу без исследования, за минуту не понять
Я же хотел именно что бы было понятно за минуту и запуск в одну строку. Ну почти в одну)
В вашем примере есть
@Inject GitHub gitHubService;
Тут явно более сложная реализация или даже сказать применение. Может быть ее и удобнее и полезнее было применить в вашем случае
igla
Синглтон!(