Pull to refresh

Android Volley custom Loader

Reading time 8 min
Views 12K
В статье изложен подход реализации Loader для загрузки разных объектов в одном Activity. В качестве сетевой библиотеки загрузки используется Volley. Метод подходит когда в одном Activity имеется несколько одновременно использующихся фрагментов

public class MainActivity extends ActionBarActivity
        implements LoaderManager.LoaderCallbacks<DataHolder>{
    ...
    @Override
    public void onLoadFinished(Loader<DataHolder> loader, DataHolder data) {
        if ( loader.getId() == DataLoader.LOADER_ICONS_ID ){
           doIcons( data.getIcons() );
        } else if( loader.getId() == DataLoader.LOADER_STYLES_ID ){
           doStyles( data.getStyles() );
        } else if( loader.getId() == DataLoader.LOADER_ICONSETS_ID ){
           doIconSets( data.getIconSets() );
        }



Основная проблема использования Fragment это то что они «живут своей жизнью» (Поправьте меня если это не так). Особенно в момент поворота экрана. Отсюда и конструктор без параметров и static newInstance(..)
newInstance
    /**
     * Returns a new instance of this fragment
     *
     */
    public static IconsGridFragment newInstance(Icons icons) {
        IconsGridFragment fragment = new IconsGridFragment();
        Bundle args = new Bundle();
        args.putParcelable(ARG_ICONS, icons);
        fragment.setArguments(args);
        return fragment;
    }
    public IconsGridFragment() {
    }


All subclasses of Fragment must include a public no-argument constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the no-argument constructor is not available, a runtime exception will occur in some cases during state restore.

Поэтому используя асинхронную загрузку с помошью Retrofit или Volley нельзя на сто процентов быть уверенным во время возврата из callback в каком состоянии Activity и Fragment. Есть внутренние состояния для FragmentManager, которые можно проверить, но это плохой подход. Например:

// Resolved After Loader implementation
if( !fragmentManager.isDestroyed() ) {    // Check problem after rotation screen


Поэтому было решено написать собственный Loader. Feed для теста был выбран Iconfinder. Скажу что feed отдается не всегда по запросу без ошибок
E/Volley﹕ [979] BasicNetwork.performRequest: Unexpected response code 429 for https...
Например можно сделать около 100 запросов, а потом вернется ошибка. Была попытка написать в службу поддержки, но ответа не последовало. Через ~5 секунд ошибка пропадает и возобновляются нормальные запросы

Ответили из Iconfinder: The 429 status code means you're making too many requests: developer.iconfinder.com/api/2.0/overview.html#rate-limiting
Мог бы и сам догадаться. По этому поводу сделал всплывающее окно с анимацией, все так же через фрагмент. Закрытие окна и сброс флажка загрузки после 5 секунд. Отложеное сообщение на закрытие вызывается сразу после совершения ft.commit(). По правильному надо бы по событию о присоединении фрагмента вызывать. Callback из onAttach например.
OverlayMessageFragment using
    private void closeOverlayDelay() {
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            public void run() {
                closeOverlayFragment();
                // Reset flag fo continue load after error gone
                Fragment fragment = getSupportFragmentManager().
                        findFragmentByTag(IconsGridFragment.class.getSimpleName());
                if (fragment != null)
                    ((IconsGridFragment)fragment).resetLoadingFlag();
            }
        }, 5000);
    }

    @Override
    public void onLoadFinished(final Loader<DataHolder> loader, final DataHolder data) {
        VolleyError volleyError = data.getError();
        if (volleyError != null) {
            if (DEBUG) Log.e(TAG, "volleyError message: " + volleyError.getMessage());
            NetworkResponse networkResponse = volleyError.networkResponse;
            if (networkResponse != null && networkResponse.statusCode == 429) {
                // HTTP Status Code: 429
                if (DEBUG) Log.e(TAG, "volleyError statusCode: " + networkResponse.statusCode);
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        Fragment fragment = getSupportFragmentManager().
                                findFragmentByTag(OverlayMessageFragment.class.getSimpleName());
                        if(fragment == null) {
                            Fragment overlayMessageFragment = OverlayMessageFragment.newInstance("Server Error 429. Too many requests. Try later");
                            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
                            ft.setCustomAnimations(R.anim.enter, R.anim.exit, R.anim.pop_enter, R.anim.pop_exit);
                            ft.add(R.id.container, overlayMessageFragment, OverlayMessageFragment.class.getSimpleName());
                            ft.commit();
                            closeOverlayDelay();
                        }
                    }
                });
                return;
            }
            return;
        }
...



Сам проект доступен на github Android Iconfinder demo

App-z.net

Loader выглядит следующим образом:
public class DataLoader extends Loader<DataHolder> {
    public static final String ARGS_URL = "url";
    private String urlFeed;
    private RequestQueue requestQueue;
    private DataHolder dataHolder = new DataHolder();

    public static final int LOADER_ICONS_ID = 1;
    public static final int LOADER_STYLES_ID = 2;
    public static final int LOADER_ICONSETS_ID = 3;

    public DataLoader(Context context, Bundle bundle) {
        super(context);
        urlFeed = bundle.getString(ARGS_URL);
        requestQueue = Volley.newRequestQueue(context);
        // run only once
        onContentChanged();
    }

    @Override
    protected void onStartLoading() {
        if (takeContentChanged())
            forceLoad();
    }

    @Override
    protected void onStopLoading() {
        requestQueue.cancelAll(this);
        super.onStopLoading();
    }

    @Override
    protected void onReset() {
        requestQueue.cancelAll(this);
        super.onReset();
    }

    @Override
    public void onForceLoad() {
        super.onForceLoad();
        if( getId() == LOADER_STYLES_ID )
            doStylesRequest();
        else if ( getId() == LOADER_ICONS_ID )
            doIconsRequest();
        else if ( getId() == LOADER_ICONSETS_ID )
            doIconsetsRequest();
    }

    private void doIconsetsRequest() {
        final GsonRequest gsonRequest = new GsonRequest(urlFeed, Iconsets.class, null, new Response.Listener<Iconsets>() {
            @Override
            public void onResponse(Iconsets iconsets) {
                dataHolder.setIconsets(iconsets);
                deliverResult(dataHolder);
            }
        ...
    }
    void doStylesRequest(){
      ...
    }
    void  doIconsRequest(){
      ...
    }
}


Есть момент. Вернуть данные сразу из callback Volley не получится или правильнее сказать создать на лету DataHolder для deliverResult, поэтому нужен именно член класса DataHolder в котором будут хранится ссылки на объекты
Не забываем про requestQueue.cancelAll(this); когда Loader прерывает работу
Также для Volley сделана небольшая обертка GsonRequest

Но и это еще не все. После вызова onLoadFinished сразу запустить Fragment не получится. Потребуется дополнительная реализация через Handler. На stackoverflow пишут о баге и предлагают именно такое решение:
Handler
    final int ICONS_HANDLER = 1;
    final int STILES_HANDLER = 2;
    final int ICONSETS_HANDLER = 3;

    @Override
    public void onLoadFinished(Loader<DataHolder> loader, DataHolder data) {
        if(data == null ) {
            // In Loader happened error
            AppUtils.showDialog(MainActivity.this, "Error", "Server request error. Try again later", false);
            return;
        }

        Message msg = mHandler.obtainMessage();
        Bundle b = new Bundle();
        if(loader.getId() == DataLoader.LOADER_ICONS_ID){
            offset += count;    // Prepare for next lazy load
            b.putParcelable("Icons", data.getIcons());
            msg.what = ICONS_HANDLER;
        } else if(loader.getId() == DataLoader.LOADER_STYLES_ID){
            b.putParcelable("Styles", data.getStyles());
            msg.what = STILES_HANDLER;
        } else if(loader.getId() == DataLoader.LOADER_ICONSETS_ID){
            b.putParcelable("IconSets", data.getIconSets());
            msg.what = ICONSETS_HANDLER;
        }
        msg.setData(b);
        mHandler.sendMessage(msg);
    }

    final Handler mHandler = new Handler(){
        public void handleMessage(Message msg) {
            Bundle b;
            b=msg.getData();
            if(msg.what == ICONS_HANDLER){
                Icons icons = b.getParcelable("Icons");
                fillIcons(icons);
            } else if(msg.what == STILES_HANDLER){
                Styles styles = b.getParcelable("Styles");
                fillStyles(styles);
            } else if(msg.what == ICONSETS_HANDLER){
                Iconsets iconSets = b.getParcelable("IconSets");
                fillIconSets(iconSets);
            }
            super.handleMessage(msg);
        }
    };


Остается добавить сам Fragment в BackStack. Причем предварительно проверяем, создан ли он уже и если создан просто добавляем иконки в Adapter. Таким образом реализована подобие pagination или как я назвал LazyLoadMore. Подгрузка иконок и фида в фоне
addToBackStack
private void fillIcons(Icons icons) {
        // Resolved After Loader implementation
        //if(!fragmentManager.isDestroyed()) {    // Check problem after rotation screen
        FragmentManager fragmentManager = getSupportFragmentManager();
        Fragment iconsGridFragment = fragmentManager.findFragmentByTag(IconsGridFragment.class.getSimpleName());
        if (iconsGridFragment != null) {
            ((IconsGridFragment) iconsGridFragment).addIcons(icons);
        } else {
            iconsGridFragment = IconsGridFragment.newInstance(icons);
            // Add the fragment to the activity, pushing this transaction on to the back stack.
            FragmentTransaction ft = fragmentManager.beginTransaction();
            ft.replace(R.id.container, iconsGridFragment, IconsGridFragment.class.getSimpleName());
            ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
            ft.addToBackStack(null);
            ft.commit();
        }
    }


В MainActivity.onStop () прерываем все загрузки:
private void destroyLoaders(){
     mHandler.removeCallbacksAndMessages(null);   // Because using Fix !
     LoaderManager loaderManager = getSupportLoaderManager();
     loaderManager.destroyLoader(DataLoader.LOADER_ICONS_ID);
     loaderManager.destroyLoader(DataLoader.LOADER_ICONSETS_ID);
     loaderManager.destroyLoader(DataLoader.LOADER_STYLES_ID);
    }
    @Override
    protected void onStop () {
        super.onStop();
        destroyLoaders();
    ...


Все методы для загрузки однотипные. Следующий шаг реализовать универсальный типовой метод запроса используя
Class<T> clazz;

Выглядит это так:
@Override
public void onForceLoad() {
    super.onForceLoad();
    doRequest(DataHolder.getClazz(getId()));
}

Классы в DataHolder должны быть implements DataHolderItem
public class Icons implements Parcelable, DataHolder.DataHolderItem{

DataLoader
/**
 * Created by App-z.net on 02.04.15.
 */
public class DataLoader extends Loader<DataHolder> {
    private static final boolean DEBUG = true;
    private static final String TAG = "DataLoader>";

    public static final String ARGS_URL = "url";

    private String urlFeed;

    private RequestQueue requestQueue;
    private DataHolder dataHolder = new DataHolder();

    public DataLoader(Context context, Bundle bundle) {
        super(context);
        urlFeed = bundle.getString(ARGS_URL);
        requestQueue = Volley.newRequestQueue(context);
        // run only once
        onContentChanged();
    }


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

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


DataHolder
/**
 * Created by App-z.net on 02.04.15.
 */
public class DataHolder {
    public static final int LOADER_ICONS_ID = 1;
    public static final int LOADER_STYLES_ID = 2;
    public static final int LOADER_ICONSETS_ID = 3;

    private Styles styles;
    private Icons icons;
    private Iconsets iconsets;

    public Styles getStyles(){
        return styles;
    }

    private void setStyles(Styles styles){
        this.styles = styles;
    }

    public Iconsets getIconsets() {
        return iconsets;
    }

    private void setIconsets(Iconsets iconsets){
        this.iconsets = iconsets;
    }

    public Icons getIcons(){
        return icons;
    }

    private void setIcons(Icons icons){
        this.icons = icons;
    }

    public void setData(int dataId, DataHolderItem item){
        switch (dataId){
            case LOADER_ICONS_ID:
                setIcons((Icons)item);
                break;
            case LOADER_STYLES_ID:
                setStyles((Styles)item);
                break;
            case LOADER_ICONSETS_ID:
                setIconsets((Iconsets)item);
                break;
            default:
                assert false : "Error LOADER ID";
        }
    }

    public DataHolderItem getData(int dataId){
        switch (dataId){
            case LOADER_ICONS_ID:
                return getIcons();
            case LOADER_STYLES_ID:
                return getStyles();
            case LOADER_ICONSETS_ID:
                return getIconsets();
            default:
                assert false : "Error LOADER ID";
        }
        return null;
    }

    public static Class<?> getClazz(int dataId){
            switch (dataId){
                case LOADER_ICONS_ID:
                    return Icons.class;
                case LOADER_STYLES_ID:
                    return Styles.class;
                case LOADER_ICONSETS_ID:
                    return Iconsets.class;
                default:
                    assert false : "Error LOADER ID";
            }
            return null;
        }

    public interface DataHolderItem{
    }
}


Повторюсь, полная реализация проекта доступна на github Android Iconfinder demo. Api iconfinder Api 2.0

Таким образом получилось приложение с одним Activity и «бутербродом» из Fragments с корректным поворотом экрана

Список литературы:
Loader
Fragment
Implementing Loaders
Tags:
Hubs:
+8
Comments 10
Comments Comments 10

Articles