В статье изложен подход реализации Loader для загрузки разных объектов в одном Activity. В качестве сетевой библиотеки загрузки используется Volley. Метод подходит когда в одном Activity имеется несколько одновременно использующихся фрагментов
Основная проблема использования 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, которые можно проверить, но это плохой подход. Например:
Поэтому было решено написать собственный 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 например.
Сам проект доступен на github Android Iconfinder demo
Loader выглядит следующим образом:
Есть момент. Вернуть данные сразу из callback Volley не получится или правильнее сказать создать на лету DataHolder для deliverResult, поэтому нужен именно член класса DataHolder в котором будут хранится ссылки на объекты
Не забываем про requestQueue.cancelAll(this); когда Loader прерывает работу
Также для Volley сделана небольшая обертка GsonRequest
Но и это еще не все. После вызова onLoadFinished сразу запустить Fragment не получится. Потребуется дополнительная реализация через Handler. На stackoverflow пишут о баге и предлагают именно такое решение:
Остается добавить сам Fragment в BackStack. Причем предварительно проверяем, создан ли он уже и если создан просто добавляем иконки в Adapter. Таким образом реализована подобие pagination или как я назвал LazyLoadMore. Подгрузка иконок и фида в фоне
В MainActivity.onStop () прерываем все загрузки:
Все методы для загрузки однотипные. Следующий шаг реализовать универсальный типовой метод запроса используя
Выглядит это так:
Классы в DataHolder должны быть implements DataHolderItem
Повторюсь, полная реализация проекта доступна на github Android Iconfinder demo. Api iconfinder Api 2.0
Таким образом получилось приложение с одним Activity и «бутербродом» из Fragments с корректным поворотом экрана
Список литературы:
Loader
Fragment
Implementing Loaders
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
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
Gwindor
А чем вам сервисы не угодили? Или это в целях эксперемента?
app-z Автор
Вы предлагаете держать сервис в фоне и общаться с ним?
developer.android.com/guide/components/services.html
Поделитесь идеей подробнее
Gwindor
Завязывая себя на активити вы лишаете себя такой вещи как кеширование, предзагрузка, да и гибкости в целом. Все библиотеки используют статический подход, что бы не требовать дополнительной инициализации в манифесте (для удобства пользователя либы). Лучше использовать класический подход Activity (pзапрос севису на загрузку и подписка на изменения Content Provider) -> Service (загрузка c изменением статуса в Content Provider). Это при условии что вы не хотите использовать библтотеки. Да и библиотеки в любом случае лучше использовать в сервисе, хотя статический подход библиотек дает им возможность отвязаться от жизненого цикла активити, предоставляя тот или иной механизм колбеков.
app-z Автор
Судя по вашей логике сама концепция Loaders's это эксперимент
Обмен с Cursor реализовать через ContentProvider и локальную базу данных. AsyncTaskLoader тогда тоже не нужен, все в Servis переносим
Кеширование Loader как раз позволяет реализовать
Предзагрузка, это в какой момент? После включения телефона? Loader можно запустить в Activity.onCreate
Gwindor
Как вариант — да, но в Android и других собитий полно.
Loaders полезны и их нужно использовать, но не для работы с сетью, как мне кажеться.
zagayevskiy
Loaders — это для «коротких» загрузок. Общение с БД, загрузка больших файлов, etc.
7voprosov
У некоторых действительно возникают крамольные мысли относительно того насколько Loader-ы (ровно как и AsyncTask-и ) являются элегантным механизмом работы с асинхронными задачами.