I want to implement an AsyncTaskLoader in my project using the Compatibility Package, so I followed the Loader manual in Android Docs.
The problem is that the Loader does nothing, it seems loadInBackground() is never called
Any idea of what's wrong in my code?
(ExpandableListFragment extends Fragment,but doesn't override any critical method )
Thank you :-)
/**EDIT:
I realized (late, I'm a moron) that AsyncTaskLoader is an abstract class so I need to subclass it... m(__)m
I leave the question in case someone comes here behind me, who knows...
public class AgendaListFragment extends ExpandableListFragment implements
LoaderManager.LoaderCallbacks<JSONArray> {
private TreeMap<Integer, ArrayList<Evento>> mItems = new TreeMap<Integer, ArrayList<Evento>>();
private AgendaListAdapter mAdapter;
private ProgressBar mProgressBar;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_agenda, container);
mProgressBar = (ProgressBar) root.findViewById(R.id.loading);
return root;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mAdapter = new AgendaListAdapter(getActivity());
setListAdapter(mAdapter);
getLoaderManager().initLoader(0, null, this);
}
#Override
public Loader<JSONArray> onCreateLoader(int arg0, Bundle arg1) {
mProgressBar.setVisibility(View.VISIBLE);
return new AsyncTaskLoader<JSONArray>(getActivity()) {
#Override
public JSONArray loadInBackground() {
return getDataFromService(AgendaServices.LISTADO_MES);
}
};
}
#Override
public void onLoadFinished(Loader<JSONArray> loader, JSONArray data) {
// Some stuff to turn JSONArray into TreeMap
mProgressBar.setVisibility(View.GONE);
mAdapter.setItems(mItems);
}
#Override
public void onLoaderReset(Loader<JSONArray> arg0) {
mAdapter.setItems(null);
mProgressBar.setVisibility(View.VISIBLE);
}
}
I think the best solution for the Compatibility package is to override the AsyncTaskLoader.onStartLoading method.
e.g.
#Override
protected void onStartLoading() {
if(dataIsReady) {
deliverResult(data);
} else {
forceLoad();
}
}
This is exactly a fix but it should work. I am pretty sure the compatibility library is broken. Try this:
getLoaderManager().initLoader(0, null, this).forceLoad();
Cheok Yan Cheng is absolutely right:
Checking for takeContentChanged seems an important step too.
If you write your method like this:
protected void onStartLoading() {
forceLoad();
}
you ''ll notice that when a child activity comes up and then you return to the parent one, onStartLoading (and so loadInBackground) are called again!
What can you do?
Set an internal variable (mContentChanged) to true inside the constructor; then check this variable inside onStartLoading. Only when it's true, start loading for real:
package example.util;
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
public abstract class ATLoader<D> extends AsyncTaskLoader<D> {
public ATLoader(Context context) {
super(context);
// run only once
onContentChanged();
}
#Override
protected void onStartLoading() {
// That's how we start every AsyncTaskLoader...
// - code snippet from android.content.CursorLoader (method onStartLoading)
if (takeContentChanged()) {
forceLoad();
}
}
}
Looking at discussion at https://code.google.com/p/android/issues/detail?id=14944, checking for takeContentChanged seems to be important step too.
protected void onStartLoading() {
if (mCursor != null) {
deliverResult(mCursor);
}
if (takeContentChanged() || mCursor == null) {
forceLoad();
}
}
I took the source code of CursorLoader from android framework, and wrote a CustomTaskLoader<T> class to ease the job.
https://github.com/Palatis/danbooru-gallery-android/blob/new_api/DanbooruGallery/src/main/java/tw/idv/palatis/danboorugallery/android/content/CustomTaskLoader.java
you basically implement these two functions:
public abstract T runTaskInBackground(CancellationSignal signal);
public abstract void cleanUp(T oldResult);
see the usage in the activities and fragments, for example this one:
(well my code just ignores the CancellationSignal, it's a TODO in my list, but you're free to use it.)
https://github.com/Palatis/danbooru-gallery-android/blob/new_api/DanbooruGallery/src/main/java/tw/idv/palatis/danboorugallery/PostListFragment.java
return new CustomTaskLoader<Cursor>(getActivity().getApplicationContext())
{
#Override
public Cursor runTaskInBackground(CancellationSignal signal)
{
return SiteSession.getAllPostsCursor(PostListAdapter.POST_COLUMNS);
}
#Override
public void cleanUp(Cursor oldCursor)
{
if (!oldCursor.isClosed())
oldCursor.close();
}
}
I have had the same problem after migrating from CursorLoader to AsyncTaskLoader.
documentation says: Subclasses of Loader<D> generally must implement at least onStartLoading(), onStopLoading(), onForceLoad(), and onReset().
AsyncTaskLoader extends Loader but not implements onStartLoading(), onStopLoading(), onReset(). You must implement it by yourself!
#davidshen84 proposed good solution. I only added checking for takeContentChanged.
#Override
protected void onStartLoading() {
try {
if (data != null) {
deliverResult(data);
}
if (takeContentChanged() || data == null) {
forceLoad();
}
Log.d(TAG, "onStartLoading() ");
} catch (Exception e) {
Log.d(TAG, e.getMessage());
}
}
Using forceLoad() is ok (not a bad practice). See what documentation says:
You generally should only call this when the loader is started - that is, isStarted() returns true.
I was still having the problem that loading of data was not called. I finally removed the AsyncTaskLoader (the support library version) and used only AsyncTask (not from support library) to do the job. And it worked.
It could be enough for your needs too.
Description and example: http://developer.android.com/reference/android/os/AsyncTask.html.
You have to extend the class AsyncTask.
The method doInBackground will do the work and in the method onPostExecute you will get the result. For starting the AsyncTask, you will call the method execute on its instance. See the link.
Related
I'm writing an Android application that uses an AsyncTaskLoader handled by a LoaderManager to acquire some data. The data can be modified upstream when the app is open, but as loading the data is time-consuming I check if it has been modified first.
I cache the result and the last-modified field, and my loadInBackground() method first checks if the upstream data has been modified before loading the actual data. Checking the upstream last-modified field is also time-consuming, and therefore must be done inside the AsyncTaskLoader, not on the UI thread.
public class DataActivity extends Activity implements LoaderManager.LoaderCallbacks<LoadedData> {
// ...
#Override
protected void onCreate(Bundle savedInstanceState) {
// ...
getLoaderManager().initLoader(0, null, this);
}
private void reloadData() { // called from various locations
getLoaderManager().restartLoader(0, null, this);
}
#Override
public Loader<LoadedData> onCreateLoader(int i, Bundle bundle) {
return new DataLoader(this);
}
#Override
public void onLoadFinished(Loader<LoadedData> loader, LoadedData result) {
setActivityLoadingState(false);
updateShownData(result);
}
#Override
public void onLoaderReset(Loader<LoadedData> loader) {}
private static class DataLoader extends AsyncTaskLoader<LoadedData> {
private LoadedData lastData;
private int lastModified = -1;
GameListLoader(DataActivity activity) {
super(activity);
}
#Override
public LoadedData loadInBackground() {
int currentModified = getUpstreamLastModified();
if (currentModified == lastModified)
return lastData;
LoadedData currentData = getUpstreamData();
lastData = currentData;
lastModified = currentModified;
return currentData;
}
#Override
protected void onStartLoading() {
forceLoad();
setActivityLoadingState(true);
}
}
}
Now, I noticed that the LoaderManager.restartLoader method creates a new Loader every time, which discards my cache entirely, and loads the data every time.
Is there a way to ask the AsyncTaskLoader to refresh (i.e. call its startLoading, as I have onStartLoading calling forceLoad) from the LoaderManager? Or should I not be using LoaderManager or AsyncTaskLoader at all?
I am using the support library and am using an AsyncTaskLoader. Why doesn't the constructor of the AsycTaskLoader not accepting a Fragment as a parameter?
I only want the AsyncTaskLoader to start loading data when it is called or initialized inside my fragments. But as of now, at anytime the Activity goes to onResume it restarts all the loaders I initialized on different fragments. I believe this is mainly because I am passing fragment.getActivity() in the constructor of my AsyncTaskLoader instances.
Any way to do this?
So far, I am wrapping the initialization of the loaders in a fragment and each have an inner AsyncTaskLoader, which I customized as well. Then when the fragment is initialized, in the onCreateView method, I then call the method like so :
initLoader();
initLoader() method
public Loader<Object> initLoader() {
return getLoaderManager().initLoader(LOADER_ID.DUMMIES, null, new LoaderCallbacks<Object>() {
#Override
public Loader<Object> onCreateLoader(int id, Bundle args) {
Loader<Object> loader = new CustomLoader<Object>(getActivity()) {
#Override
public Object loadInBackground() {
return DummyGenerator.generateDummyEntriesToDb();;
}
};
return loader;
}
#Override
public void onLoadFinished(Loader<Object> loader, Object data) {
setToDb(data);
}
#Override
public void onLoaderReset(Loader<Object> loader) {
}
});
}
CustomLoader.java - generic implementation I suited to my needs. The releaseResources method is not filled in but I left it there for future usage.
public class CustomLoader<T> extends AsyncTaskLoader<T> {
T mData;
public CustomLoader(Context context) {
super(context);
}
#Override
public T loadInBackground() {
return null;
}
#Override
public void deliverResult(T data) {
if (isReset()) {
releaseResources(data);
return;
}
T oldData = mData;
mData = data;
if (isStarted()) {
super.deliverResult(data);
}
if (oldData != null && oldData != data) {
releaseResources(oldData);
}
}
#Override
protected void onStartLoading() {
if (mData != null) {
deliverResult(mData);
}
if (takeContentChanged() || mData == null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
cancelLoad();
}
#Override
protected void onReset() {
onStopLoading();
if (mData != null) {
releaseResources(mData);
mData = null;
}
}
#Override
public void onCanceled(T data) {
super.onCanceled(data);
releaseResources(data);
}
public void releaseResources(T data) {
}
}
The generateDummyEntriesToDb method is working fine just creating list of the objects I am using, as well as the setToDb() method. The problem is when the activity goes to onResume the loadnBackground() method is called again thus I am compelled to think that all the other loaders behave the same way.
The Context provided should be the Activity attached to the Fragment. Be sure you are initializing the loader via the LoaderManager as it is tied to the appropriate object life cycle. So when initializing from within a Fragment, you should use Fragment.getLoaderManager(). Then call LoaderManager.initLoader() appropriately.
According to http://developer.android.com/guide/components/loaders.html, one of the nice thing about loader is that, it is able to retain its data during configuration change.
They automatically reconnect to the last loader's cursor when being
recreated after a configuration change. Thus, they don't need to
re-query their data.
However, it doesn't work well in all scenarios.
I take a following simple example. It is a FragmentActivity, which is hosting a Fragment. The Fragment itself owns the AsyncTaskLoader.
The following 3 scenarios work pretty well.
During first launched (OK)
1 loader is created, and loadInBackground is executed once.
During simple rotation (OK)
No new loader is being created and loadInBackground is not being triggered.
A child activity is launched, and back button pressed (OK)
No new loader is being created and loadInBackground is not being triggered.
However, in the following scenario.
A child activity is launched -> Rotation -> Back button pressed (Wrong)
At that time, old loader's onReset is called. Old loader will be destroyed. New loader will be created and new loader's loadInBackground will be triggered again.
The correct behavior I'm expecting is, no new loader will be created.
The loader related code is as follow. I run the code under Android 4.1 emulator.
package com.example.bug;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class MainFragment extends Fragment implements LoaderManager.LoaderCallbacks<Integer> {
private static class IntegerArrayLoader extends AsyncTaskLoader<Integer> {
private Integer result = null;
public IntegerArrayLoader(Context context) {
super(context);
Log.i("CHEOK", "IntegerArrayLoader created!");
}
#Override
public Integer loadInBackground() {
Log.i("CHEOK", "Time consuming loadInBackground!");
this.result = 123456;
return result;
}
/**
* Handles a request to cancel a load.
*/
#Override
public void onCanceled(Integer integer) {
super.onCanceled(integer);
}
/**
* Handles a request to stop the Loader.
* Automatically called by LoaderManager via stopLoading.
*/
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
/**
* Handles a request to start the Loader.
* Automatically called by LoaderManager via startLoading.
*/
#Override
protected void onStartLoading() {
if (this.result != null) {
deliverResult(this.result);
}
if (takeContentChanged() || this.result == null) {
forceLoad();
}
}
/**
* Handles a request to completely reset the Loader.
* Automatically called by LoaderManager via reset.
*/
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
// At this point we can release the resources associated with 'apps'
// if needed.
this.result = null;
}
}
#Override
public Loader<Integer> onCreateLoader(int arg0, Bundle arg1) {
Log.i("CHEOK", "onCreateLoader being called");
return new IntegerArrayLoader(this.getActivity());
}
#Override
public void onLoadFinished(Loader<Integer> arg0, Integer arg1) {
result = arg1;
}
#Override
public void onLoaderReset(Loader<Integer> arg0) {
// TODO Auto-generated method stub
}
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_main, container, false);
return v;
}
// http://stackoverflow.com/questions/11293441/android-loadercallbacks-onloadfinished-called-twice
#Override
public void onResume()
{
super.onResume();
if (result == null) {
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
} else {
// Restore from previous state. Perhaps through long pressed home
// button.
}
}
private Integer result;
}
Complete source code can be downloaded from https://www.dropbox.com/s/n2jee3v7cpwvedv/loader_bug.zip
This might be related to 1 unsolved Android bug : https://code.google.com/p/android/issues/detail?id=20791&can=5&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars
https://groups.google.com/forum/?fromgroups=#!topic/android-developers/DbKL6PVyhLI
I was wondering, is there any good workaround on this bug?
My answer is quite straight forward actually. Don't use AsyncTaskLoaders. Because a few bugs regarding AsyncTaskLoaders you knew it by now.
A good combination would be a retainable (setRetainInstance(true) in onActivityCreated()) fragment with AsyncTask. Works the same way. Just have to restructure the code a bit.
Message from OP
Although the author doesn't provide any code example, this is the closest workable solution. I do not use the author proposed solution. Instead, I still rely on AsyncTaskLoader for all the necessary loading task. The workaround is that, I will rely on an additional retained fragment, to determine whether I should reconnect/create loader. The is the skeleton code on the whole idea. Works pretty well so far as long as I can tell.
#Override
public void onActivityCreated(Bundle savedInstanceState) {
...
dataRetainedFragment = (DataRetainedFragment)fm.findFragmentByTag(DATE_RETAINED_FRAGMENT);
// dataRetainedFragment can be null still...
}
#Override
public void onResume() {
...
if (this.data == null) {
if (dataRetainedFragment != null) {
// Re-use!
onLoadFinished(null, dataRetainedFragment);
} else {
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
} else {
}
}
#Override
public void onLoadFinished(Loader<Data> arg0, Data data) {
this.data = data;
if (this.dataRetainedFragment == null) {
this.dataRetainedFragment = DataRetainedFragment.newInstance(this.data);
FragmentManager fm = getFragmentManager();
fm.beginTransaction().add(this.dataRetainedFragment, DATE_RETAINED_FRAGMENT).commitAllowingStateLoss();
}
Try to change,
#Override
public void onResume()
{
super.onResume();
if (result == null) {
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
} else {
// Restore from previous state. Perhaps through long pressed home
// button.
}
}
to
#Override
public void onResume()
{
super.onResume();
Loader loader = getLoaderManager().getLoader(0);
if ( loader != null && loader.isReset() ) {
getLoaderManager().restartLoader(0, getArguments(), this);
} else {
getLoaderManager().initLoader(0, getArguments(), this);
}
}
If you are using FragmentManager's replace fragment technique this issue will happen.
When you replace/remove the Fragment, the fragment is detached from the activity and since loaders are attached to the activity, the loaders will be recreated during orientation change.
Try using FragmentManager's hide/show technique. May be this will help you.
I've had success subclassing AsyncTaskLoader and making a few tweaks to its methods.
public class FixedAsyncTaskLoader<D> extends AsyncTaskLoader<D> {
private D result;
public FixedAsyncTaskLoader(Context context) {
super(context);
}
#Override
protected void onStartLoading() {
if (result != null) {
deliverResult(result);
} else {
forceLoad();
}
}
#Override
public void deliverResult(T data) {
result = data;
if (isStarted()) {
super.deliverResult(result);
}
}
#Override
protected void onReset() {
super.onReset();
onStopLoading();
result = null;
}
#Override
protected void onStopLoading() {
cancelLoad();
}
}
I have a SherlockListFragment that implements a custom AsyncTaskLoader.
In the overridden onStartLoading(), I have:
#Override
protected void onStartLoading() {
if (mData != null) {
deliverResult(mData);
}
else{
forceLoad();
}
}
The containing SherlockListFragment initiates the loader in onActivityCreated:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mAdapter = new MyListAdapter(getActivity());
setListAdapter(mAdapter);
getLoaderManager().initLoader(0, null, this);
}
and :
#Override
public Loader<List<MyData>> onCreateLoader(int id, Bundle args) {
return new MyListLoader(getActivity());
}
The problem is that after 5 activations/navigations to my FragmentActivity, loadinBackground() is not called. The onStartLoding is called, as well as the forceLoad, but that's it.
No Exception, nothing in the LogCat.
Any ideas?
It is Ok to call forceLoad().
See what documentation says:
You generally should only call this when the loader is started - that is, isStarted() returns true.
Full code:
#Override
protected void onStartLoading() {
try {
if (data != null) {
deliverResult(data);
}
if (takeContentChanged() || data == null) {
forceLoad();
}
Log.d(TAG, "onStartLoading() ");
} catch (Exception e) {
Log.d(TAG, e.getMessage());
}
}
Important:
documentation says: Subclasses of Loader<D> generally must implement at least onStartLoading(), onStopLoading(), onForceLoad(), and onReset().
AsyncTaskLoader extends Loader but not implements onStartLoading(), onStopLoading(), onReset(). You must implement it by yourself!
P.S. I was confused with it after experience of using simple CursorLoader, I also thought that using forceLoad() is bad practice. But it is not true.
Since it's not recommended to keep a strong reference to a Context in a task (the context may get destroyed while the task is still running, but kept in memory by the task), I was wondering if the same applies to Fragments?
Fragments manage their activity reference, and support being retained via setRetainInstance. Can I assume that e.g. creating a non-static inner AsyncTask in a Fragment is safe in terms of not risking to leak $this?
It's generally bad methodology to keep references between threads, and an AsyncTask is something like a thread.
It's okay, as long as you make sure that you dereference it when you are done using it.
Otherwise, you could get memory leaks.
In this case, it's okay because your Fragment is in the context of AsyncTask. When the task is done, it will lose that reference.
If this were being done in a Service, it would be a very bad idea.
Phoenixblade9's answer is correct, but to make it full I'd add one thing.
There's a great replacement for AsyncTask - AsyncTaskLoader, or Loaders generally. It manages its lifecycle according to the context from which it's been called (Activity, Fragment) and implements a bunch of listeners to help you separate the logic of a second thread from the ui thread. And it's generally immune to leaking context.
And don't bother the name - it's good for saving data as well.
As promised I'll post my code for AsyncTaskLoader withg multiple objects returned. The loader goes something like this:
public class ItemsLoader extends AsyncTaskLoader<HashMap<String, Object>>{
HashMap<String, Object> returned;
ArrayList<SomeItem> items;
Context cxt;
public EventsLoader(Context context) {
super(context);
//here you can initialize your vars and get your context if you need it inside
}
#Override
public HashMap<String, Object> loadInBackground() {
returned = getYourData();
return returned;
}
#Override
public void deliverResult(HashMap<String, Object> returned) {
if (isReset()) {
return;
}
this.returned = returned;
super.deliverResult(returned);
}
#Override
protected void onStartLoading() {
if (returned != null) {
deliverResult(returned);
}
if (takeContentChanged() || returned == null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
cancelLoad();
}
#Override
protected void onReset() {
super.onReset();
onStopLoading();
returned = null;
}
In the getYourData() function I get both the server message code or some other error code and an ArrayList<SomeItem>. I can use them in my Fragment like this:
public class ItemListFragment extends ListFragment implements LoaderCallbacks<HashMap<String, Object>>{
private LoaderManager lm;
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
lm = getLoaderManager();
Bundle args = new Bundle();
args.putInt("someId", someId);
lm.initLoader(0, args, this);
}
#Override
public Loader<HashMap<String, Object>> onCreateLoader(int arg0, Bundle args) {
ItemsLoader loader = new ItemsLoader(getActivity(), args.getInt("someId"));
return loader;
}
#Override
public void onLoadFinished(Loader<HashMap<String, Object>> loader, HashMap<String, Object> data) {
if(data!=null){ if(data.containsKey("items")){
ArrayList<SomeItem> items = (ArrayList<EventItem>)data.get("items");
} else { //error
int error = 0;
if(data.containsKey("error")){
error = (Integer) data.get("error");
}
}
}
#Override
public void onLoaderReset(Loader<HashMap<String, Object>> arg0) {
}