- PVSM.RU - https://www.pvsm.ru -

Android Volley custom Loader

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

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

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 [2]. Api iconfinder Api 2.0 [3]

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

Список литературы:
Loader [4]
Fragment [5]
Implementing Loaders [6]

Автор: app-z

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android/87862

Ссылки в тексте:

[1] Iconfinder: http://iconfinder.com

[2] Android Iconfinder demo: https://github.com/app-z/Iconfinder

[3] Api 2.0: https://developer.iconfinder.com/api/2.0/index.html

[4] Loader: http://developer.android.com/reference/android/content/Loader.html

[5] Fragment: http://developer.android.com/reference/android/app/Fragment.html

[6] Implementing Loaders: http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html

[7] Источник: http://habrahabr.ru/post/254801/