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.
1 loader is created, and loadInBackground
is executed once.
No new loader is being created and loadInBackground
is not being triggered.
No new loader is being created and loadInBackground
is not being triggered.
However, in the following scenario.
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.
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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With