Дальнейшие эксперименты по скрещиванию Volley и Loader привели меня к мысли о создании библиотеки. Чтобы вызывался Loader с параметрами и в одну строку. На подобии как это реализовано в Picaso. После пары вечеров что-то получилось…

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

FeedLoader.with(this)
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
FeedLoader.java
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;
    }
}


FeedLoaderWrapper.java
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()));
    }
}


DispatcherData.java
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);
    }
}


GsonRequest.java
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
  • Добавлен кэш запросов
Стали бы вы пользоваться такой библиотекой?

Проголосовало 45 человек. Воздержалось 19 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (9)


  1. igla
    06.04.2015 09:28
    -2

    Синглтон!(


  1. 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;
    }
    


    1. andreich
      06.04.2015 14:15

      мне кажется что activity передается, чтобы в дальнейшем сделать вот это

       ((FragmentActivity)context).getSupportLoaderManager().
                      restartLoader(loaderId, bundle, callback);
      


      1. withoutuniverse
        06.04.2015 14:41
        +1

        В таком случае, это очень и очень плохой код. Это тоже самое, что в метод передавать Object, а затем его приводить к FragmentActivity даже без проверки типа объекта.


        1. andreich
          06.04.2015 15:07

          я согласен с вами абсолютно. мне кажется автору стоит немного подумать над реализацией и привести код в порядок.


        1. 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);
          }
          


    1. app-z Автор
      06.04.2015 21:38

      Вы правы! Первоначальная цель была посмотреть как получится, потом интересно ли вообще в использовании.


  1. nekdenis
    06.04.2015 17:24

    Если уж так хочется работать с сетью через Loaders, то есть скрещенный Retrofit+AsyncTaskLoader: github.com/rciovati/retrofit-loaders-example
    Пользовался данным методом неоднократно.


    1. app-z Автор
      06.04.2015 21:36

      Retrofit сам по себе умеет делать асинхронные запросы. Поэтому оборачивать его дополнительно в AsyncTaskLoader не требуется. Если конечно не использовать не асинхронный вызов.
      Второе, на сколько понял из кода используется общий callback

      public class MyActivity extends Activity implements Callback<List<Issue>> {
      

      Или нет?
      static class IssuesLoader extends RetrofitLoader<List<Issue>, GitHub> {
      

      Сразу с ходу без исследования, за минуту не понять
      Я же хотел именно что бы было понятно за минуту и запуск в одну строку. Ну почти в одну)
      В вашем примере есть
      @Inject
          GitHub gitHubService;
      

      Тут явно более сложная реализация или даже сказать применение. Может быть ее и удобнее и полезнее было применить в вашем случае