Loader unable to retain itself during certain configuration change - android

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();
}
}

Related

Refreshing a Loader without discarding previous data

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?

Android Loader vs AsyncTask on button tap

I have an activity which requires no data from server on load - just plain init for ui
UI has several buttons.
User clicks one of them and app sends request to server (rest call)
While request is processing spinner is shown (for about 10 seconds)
For now it uses AsyncTask - so if app changes portrait to landscape - activity is restarted and I loose the process
Second option is to use Loader - the problem is that it is started on button tap - not on activity start
This leads to many exceptions - when LoaderManager sends events to non-started item
Is there any solution?
few comments:
- 10 seconds is just for example
- lock user to one orientation is not an option
- service is overkill for simple rest call
public class TestActivity extends FragmentActivity {
private Button one;
private Button two;
private final int ONE_ID = 0;
private final int TWO_ID = 1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
one = (Button) findViewById(R.id.one);
two = (Button) findViewById(R.id.two);
one.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
getLoaderManager().restartLoader(ONE_ID, null, callbacks);
}
});
two.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
getLoaderManager().restartLoader(ONE_ID, null, callbacks);
}
});
Loader<AsyncTaskLoaderResult<Result>> loader = getLoaderManager().getLoader(ONE_ID);
if (loader != null) {
getLoaderManager().initLoader(ONE_ID, null, callbacks);
}
loader = getLoaderManager().getLoader(TWO_ID);
if (loader != null) {
getLoaderManager().initLoader(TWO_ID, null, callbacks);
}
}
public static class AsyncTaskLoaderResult<E> {
public E data;
public Bundle args;
}
public static class Result {
}
private LoaderManager.LoaderCallbacks<AsyncTaskLoaderResult<Result>> callbacks = new LoaderManager.LoaderCallbacks<AsyncTaskLoaderResult<Result>>() {
#Override
public Loader<AsyncTaskLoaderResult<Result>> onCreateLoader(int id, Bundle args) {
/**
* according different Id, create different AsyncTaskLoader
*/
switch (id) {
case ONE_ID:
return new OneAsyncTaskLoader(TestActivity.this);
case TWO_ID:
return new TwoAsyncTaskLoader(TestActivity.this);
}
return null;
}
#Override
public void onLoadFinished(Loader<AsyncTaskLoaderResult<Result>> loader, AsyncTaskLoaderResult<Result> data) {
/**
* handle result
*/
switch (loader.getId()) {
}
getLoaderManager().destroyLoader(loader.getId());
}
#Override
public void onLoaderReset(Loader<AsyncTaskLoaderResult<Result>> loader) {
}
};
public static class OneAsyncTaskLoader extends AsyncTaskLoader<AsyncTaskLoaderResult<Result>> {
private AsyncTaskLoaderResult<Result> result;
public OneAsyncTaskLoader(Context context) {
super(context);
}
#Override
protected void onStartLoading() {
super.onStartLoading();
if (result != null) {
deliverResult(result);
} else {
forceLoad();
}
}
#Override
public AsyncTaskLoaderResult<Result> loadInBackground() {
/**
* send request to server
*/
result = new AsyncTaskLoaderResult<Result>();
result.data = null; // result.data comes from server's response
return result;
}
}
public static class TwoAsyncTaskLoader extends AsyncTaskLoader<AsyncTaskLoaderResult<Result>> {
private AsyncTaskLoaderResult<Result> result;
public TwoAsyncTaskLoader(Context context) {
super(context);
}
#Override
protected void onStartLoading() {
super.onStartLoading();
if (result != null) {
deliverResult(result);
} else {
forceLoad();
}
}
#Override
public AsyncTaskLoaderResult<Result> loadInBackground() {
/**
* send request to server
*/
result = new AsyncTaskLoaderResult<Result>();
result.data = null; // result.data comes from server's response
return result;
}
}
}
First, you can eliminate the orienatation change issue by declaring
android:configChanges="orientation"
or savedInstanceState()
But the real problem here is having the user stare at a spinner for 10 seconds. Most users aren't going to be patient enough for this. I don't know what your app is doing so its hard to give an accurate suggestion but I can say that you need to do your network stuff in your AsyncTask but allow the user to do other things
You can allow the user to do other things while the AsyncTask finishes or put that code in a [Service(http://developer.android.com/guide/components/services.html). Either way, don't make your users stare at a screen for 10 seconds of spinning...they won't be YOUR users for long
If you're using an AsyncTask for this you might want to either use a Service instead or use onRetainNonConfigurationInstance or Fragment.setRetainInstance to allow the AsyncTask to live through configuration changes.
Or disable configuration changes: I've used that in the past with some success.
Here's a good article on the subject:
http://www.javacodegeeks.com/2013/01/android-loaders-versus-asynctask.html
Anyways, as #codeMagic mentioned, AsyncTask with android:configChanges="orientation|screenSize" should be enough for you (it prevents activity from being recreated on config changes)

Android Loader not triggering callbacks on screen rotate

I am using an AsyncTaskLoader. I have an activity which has implemented LoaderCallbacks (Support library).
I have breakpoint debugged and put in logs, the loader delivers the result, however the second time the callback onLoadFinished is not triggered. The odd thing - when I rotate back again it works, which ever orientation I start on gets callbacks when I return to it.
In my Activity onResume:
LoaderManager lm = getSupportLoaderManager();
Loader loader = lm.initLoader(0, null, new LoaderManager.LoaderCallbacks<String>() {
#Override
public Loader<String> onCreateLoader(int i, Bundle bundle) {
Loader<String> loader = new TestLoader(MainActivity.this);
return loader;
}
#Override
public void onLoadFinished(Loader<String> stringLoader, String s) {
Log.d(Application.TAG, "OnLoadFinished " + s);
doStuff(s);
}
#Override
public void onLoaderReset(Loader<String> stringLoader) {
// NOP
}
});
In my loader:
public class TestLoader extends AsyncTaskLoader<String> {
private String mData;
public TestLoader(Context context) {
super(context);
}
// This get's called after a loader is initialized or a loader
// that is alive still is reset
#Override
public void onStartLoading() {
if (mData != null) { // Have our data loaded, just deliver it!
deliverResult(mData);
}
if (takeContentChanged() || mData == null) {
forceLoad();
}
}
// This is called when an Activity or Fragment requests a loader
// to be reset because they want new data
#Override
public void onReset() {
mData = null;
// Ensure that the old task is cancelled if it was running
// We do NOT have to call forceLoad here because onStartLoading
// will get called after this
cancelLoad();
}
// Here we just want to store our own data we got and reset our boolean
#Override
public void deliverResult(String data) {
Log.d(Application.TAG, "deliverResult " + data);
mData = data;
super.deliverResult(mData);
}
#Override
public String loadInBackground() {
// returns content from a content provider ...
}
}
Really baffled by this one, I am new to Android so maybe this is obvious to someone else :)
You must at least simply call getSupportLoaderManager() / getLoaderManager() in onCreate() if it's an Activity or onActivityCreated() if it's a Fragment. The actual initLoader() can be elsewhere. Otherwise the loader will be in a stopped state and won't deliver the results even though it completes the load. I suspect it's because the loader manager doesn't reattach the old loaders to the new Activity unless the above call is made in the new Activity's onCreate().
You have
Loader loader = lm.initLoader(...)
You should have
Loader loader = new LoaderManager.LoaderCallbacks(...) {...}
and in your onResume()
this.getLoaderManager().restartLoader(0, null, this.loader);
See Loaders documentation.

AsyncTaskLoader onLoadFinished with a pending task and config change

I'm trying to use an AsyncTaskLoader to load data in the background to populate a detail view in response to a list item being chosen. I've gotten it mostly working but I'm still having one issue. If I choose a second item in the list and then rotate the device before the load for the first selected item has completed, then the onLoadFinished() call is reporting to the activity being stopped rather than the new activity. This works fine when choosing just a single item and then rotating.
Here is the code I'm using. Activity:
public final class DemoActivity extends Activity
implements NumberListFragment.RowTappedListener,
LoaderManager.LoaderCallbacks<String> {
private static final AtomicInteger activityCounter = new AtomicInteger(0);
private int myActivityId;
private ResultFragment resultFragment;
private Integer selectedNumber;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myActivityId = activityCounter.incrementAndGet();
Log.d("DemoActivity", "onCreate for " + myActivityId);
setContentView(R.layout.demo);
resultFragment = (ResultFragment) getFragmentManager().findFragmentById(R.id.result_fragment);
getLoaderManager().initLoader(0, null, this);
}
#Override
protected void onDestroy() {
super.onDestroy();
Log.d("DemoActivity", "onDestroy for " + myActivityId);
}
#Override
public void onRowTapped(Integer number) {
selectedNumber = number;
resultFragment.setResultText("Fetching details for item " + number + "...");
getLoaderManager().restartLoader(0, null, this);
}
#Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new ResultLoader(this, selectedNumber);
}
#Override
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
resultFragment.setResultText(data);
}
#Override
public void onLoaderReset(Loader<String> loader) {
}
static final class ResultLoader extends AsyncTaskLoader<String> {
private static final Random random = new Random();
private final Integer number;
private String result;
ResultLoader(Context context, Integer number) {
super(context);
this.number = number;
}
#Override
public String loadInBackground() {
// Simulate expensive Web call
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Item " + number + " - Price: $" + random.nextInt(500) + ".00, Number in stock: " + random.nextInt(10000);
}
#Override
public void deliverResult(String data) {
if (isReset()) {
// An async query came in while the loader is stopped
return;
}
result = data;
if (isStarted()) {
super.deliverResult(data);
}
}
#Override
protected void onStartLoading() {
if (result != null) {
deliverResult(result);
}
// Only do a load if we have a source to load from
if (number != null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
result = null;
}
}
}
List fragment:
public final class NumberListFragment extends ListFragment {
interface RowTappedListener {
void onRowTapped(Integer number);
}
private RowTappedListener rowTappedListener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
rowTappedListener = (RowTappedListener) activity;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(getActivity(),
R.layout.simple_list_item_1,
Arrays.asList(1, 2, 3, 4, 5, 6));
setListAdapter(adapter);
}
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
ArrayAdapter<Integer> adapter = (ArrayAdapter<Integer>) getListAdapter();
rowTappedListener.onRowTapped(adapter.getItem(position));
}
}
Result fragment:
public final class ResultFragment extends Fragment {
private TextView resultLabel;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.result_fragment, container, false);
resultLabel = (TextView) root.findViewById(R.id.result_label);
if (savedInstanceState != null) {
resultLabel.setText(savedInstanceState.getString("labelText", ""));
}
return root;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("labelText", resultLabel.getText().toString());
}
void setResultText(String resultText) {
resultLabel.setText(resultText);
}
}
I've been able to get this working using plain AsyncTasks but I'm trying to learn more about Loaders since they handle the configuration changes automatically.
EDIT: I think I may have tracked down the issue by looking at the source for LoaderManager. When initLoader is called after the configuration change, the LoaderInfo object has its mCallbacks field updated with the new activity as the implementation of LoaderCallbacks, as I would expect.
public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
if (info == null) {
// Loader doesn't already exist; create.
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
if (DEBUG) Log.v(TAG, " Created new loader " + info);
} else {
if (DEBUG) Log.v(TAG, " Re-using existing loader " + info);
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
if (info.mHaveData && mStarted) {
// If the loader has already generated its data, report it now.
info.callOnLoadFinished(info.mLoader, info.mData);
}
return (Loader<D>)info.mLoader;
}
However, when there is a pending loader, the main LoaderInfo object also has an mPendingLoader field with a reference to a LoaderCallbacks as well, and this object is never updated with the new activity in the mCallbacks field. I would expect to see the code look like this instead:
// This line was already there
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
// This line is not currently there
info.mPendingLoader.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
It appears to be because of this that the pending loader calls onLoadFinished on the old activity instance. If I breakpoint in this method and make the call that I feel is missing using the debugger, everything works as I expect.
The new question is: Have I found a bug, or is this the expected behavior?
In most cases you should just ignore such reports if Activity is already destroyed.
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
if (isDestroyed()) {
Log.i("DemoActivity", "Activity already destroyed, report ignored: " + data);
return;
}
resultFragment.setResultText(data);
}
Also you should insert checking isDestroyed() in any inner classes. Runnable - is the most used case.
For example:
// UI thread
final Handler handler = new Handler();
Executor someExecutorService = ... ;
someExecutorService.execute(new Runnable() {
public void run() {
// some heavy operations
...
// notification to UI thread
handler.post(new Runnable() {
// this runnable can link to 'dead' activity or any outer instance
if (isDestroyed()) {
return;
}
// we are alive
onSomeHeavyOperationFinished();
});
}
});
But in such cases the best way is to avoid passing strong reference on Activity to another thread (AsynkTask, Loader, Executor, etc).
The most reliable solution is here:
// BackgroundExecutor.java
public class BackgroundExecutor {
private static final Executor instance = Executors.newSingleThreadExecutor();
public static void execute(Runnable command) {
instance.execute(command);
}
}
// MyActivity.java
public class MyActivity extends Activity {
// Some callback method from any button you want
public void onSomeButtonClicked() {
// Show toast or progress bar if needed
// Start your heavy operation
BackgroundExecutor.execute(new SomeHeavyOperation(this));
}
public void onSomeHeavyOperationFinished() {
if (isDestroyed()) {
return;
}
// Hide progress bar, update UI
}
}
// SomeHeavyOperation.java
public class SomeHeavyOperation implements Runnable {
private final WeakReference<MyActivity> ref;
public SomeHeavyOperation(MyActivity owner) {
// Unlike inner class we do not store strong reference to Activity here
this.ref = new WeakReference<MyActivity>(owner);
}
public void run() {
// Perform your heavy operation
// ...
// Done!
// It's time to notify Activity
final MyActivity owner = ref.get();
// Already died reference
if (owner == null) return;
// Perform notification in UI thread
owner.runOnUiThread(new Runnable() {
public void run() {
owner.onSomeHeavyOperationFinished();
}
});
}
}
Maybe not best solution but ...
This code restart loader every time, which is bad but only work around that works - if you want to used loader.
Loader l = getLoaderManager().getLoader(MY_LOADER);
if (l != null) {
getLoaderManager().restartLoader(MY_LOADER, null, this);
} else {
getLoaderManager().initLoader(MY_LOADER, null, this);
}
BTW. I am using Cursorloader ...
A possible solution is to start the AsyncTask in a custom singleton object and access the onFinished() result from the singleton within your Activity. Every time you rotate your screen, go onPause() or onResume(), the latest result will be used/accessed. If you still don't have a result in your singleton object, you know it is still busy or that you can relaunch the task.
Another approach is to work with a service bus like Otto, or to work with a Service.
Ok I'm trying to understand this excuse me if I misunderstood anything, but you are losing references to something when the device rotates.
Taking a stab...
would adding
android:configChanges="orientation|keyboardHidden|screenSize"
in your manifest for that activity fix your error? or prevent onLoadFinished() from saying the activity stopped?

AsyncTaskLoader doesn't run

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.

Categories

Resources