Android Volley custom Loader

в 3:41, , рубрики: android, loaders, Volley, Разработка под android

В статье изложен подход реализации 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(..)

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 отдается не всегда по запросу без ошибок. Например можно сделать около 100 запросов, а потом вернется ошибка. Была попытка написать в службу поддержки, но ответа не последовало. Через ~5 секунд ошибка пропадает и возобновляются нормальные запросы. Для теста сойдет

Сам проект доступен на 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);
        }
    };

В MainActivity onStop () прерываем все загрузки:

private void destroyLoaders(){
     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;

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

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

Список литературы:
Loader
Fragment
Implementing Loaders

Автор: app-z

Источник

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


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