Android Volley Loader. Движение в сторону библиотеки

в 21:45, , рубрики: android, loader, Volley, Разработка под android

Дальнейшие эксперименты по скрещиванию 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 = new HashMap<Integer, GoodsItem>();
    @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, DelivererFeedLoader.Listener callback){
    delivererFeedLoader.loaderClazzMap.put(loaderId, loaderClazz);
    delivererFeedLoader.callBackMap.put(loaderId, callback);
    delivererFeedLoader.urlFeeds.put(loaderId, url);
    return singleton;
}

Запускаем Loader через start(...):

public void start(int loaderId, final Context context) {
    Bundle bundle = new Bundle();
    bundle.putParcelable("delivererFeedLoader", delivererFeedLoader);
     ((FragmentActivity)context).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;

/**
 * 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 DelivererFeedLoader delivererFeedLoader = new DelivererFeedLoader();

    public FeedLoader(Context context) {
        this.context = context;
    }

    LoaderManager.LoaderCallbacks<Object> callback = new LoaderManager.LoaderCallbacks<Object>() {
        @Override
        public Loader<Object> onCreateLoader(int id, Bundle args) {
            return new FeedLoaderWrapper(context , args);
        }

        @Override
        public void onLoadFinished(Loader<Object> loader, Object data) {
            delivererFeedLoader.callBackMap.get(loader.getId()).onResponse(loader.getId(), data);
        }

        @Override
        public void onLoaderReset(Loader<Object> loader) {

        }
    };

    public FeedLoader addLoader(int loaderId, String url, Class<?> loaderClazz, DelivererFeedLoader.Listener callback){
        delivererFeedLoader.loaderClazzMap.put(loaderId, loaderClazz);
        delivererFeedLoader.callBackMap.put(loaderId, callback);
        delivererFeedLoader.urlFeeds.put(loaderId, url);
        return singleton;
    }

    public void start(int loaderId, final Context context) {
        Bundle bundle = new Bundle();
        bundle.putParcelable("delivererFeedLoader", delivererFeedLoader);

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

    public static FeedLoader with(Context context) {
        if (singleton == null) {
            synchronized (FeedLoader.class) {
                if (singleton == null) {
                    singleton = new FeedLoader(context);
                }
            }
        }
        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;

/**
 *
 * @param <D>
 */
class FeedLoaderWrapper<D> extends Loader<D> {

    private boolean DEBUG = true;
    private String TAG = getClass().getSimpleName();

    private DelivererFeedLoader delivererFeedLoader = new DelivererFeedLoader();

    private RequestQueue requestQueue;

    /**
     * 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);
        delivererFeedLoader = bundle.getParcelable("delivererFeedLoader");
        requestQueue = Volley.newRequestQueue(context);
        // run only once
        onContentChanged();
    }

    /**
     * Get Data
     */
    private void doRequest(Class<?> clazz) {
        String urlFeed = delivererFeedLoader.urlFeeds.get(getId());
        final GsonRequest gsonRequest = new GsonRequest(urlFeed,
                clazz,
                null,
                new Response.Listener<D>() {
                    @Override
                    public void onResponse(D data) {
                        deliverResult(data);
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                if (volleyError != null)
                    //deliverResult(null);
                    if (DEBUG) Log.e(TAG, "volleyError: " + volleyError.getMessage());
                        delivererFeedLoader.callBackMap.get(getId()).onErrorResponse(volleyError);
            }
        });
        requestQueue.add(gsonRequest);
    }

    @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 = delivererFeedLoader.urlFeeds.get(getId());
        if (DEBUG) Log.d(TAG, "Loader onForceLoad() : feedUrl = " + urlFeed);
        doRequest(delivererFeedLoader.loaderClazzMap.get(getId()));
    }
}

DelivererFeedLoader.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 DelivererFeedLoader implements Parcelable {
    HashMap<Integer, Class> loaderClazzMap = new HashMap<>();
    HashMap<Integer, Listener> callBackMap = new HashMap<>();
    HashMap<Integer, String> urlFeeds = new HashMap<>();

    public DelivererFeedLoader(){}

    protected DelivererFeedLoader(Parcel in) {
        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(loaderClazzMap);
        dest.writeValue(callBackMap);
        dest.writeValue(urlFeeds);
    }

    @SuppressWarnings("unused")
    public static final Parcelable.Creator<DelivererFeedLoader> CREATOR = new Parcelable.Creator<DelivererFeedLoader>() {
        @Override
        public DelivererFeedLoader createFromParcel(Parcel in) {
            return new DelivererFeedLoader(in);
        }

        @Override
        public DelivererFeedLoader[] newArray(int size) {
            return new DelivererFeedLoader[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.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;

    /**
     * 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,
                       Response.Listener<T> listener, Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        this.clazz = clazz;
        this.headers = headers;
        this.listener = listener;
    }

    @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),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JsonSyntaxException e) {
            return Response.error(new ParseError(e));
        }
    }
}

Проект на GitHub
RoboSpice

// TO DO

  • Пока нет проверок на ошибки добавляемых параметров в FeedLoader
  • Остановка Loader
  • Rename DelivererFeedLoader to DispatcherLoader

Автор: app-z

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js