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)
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?
Hi i have a function to get users from website database
my function
private void get_users() {
try {
url = "my address";
dbGetData3 = new DbGetData();
new Thread(new Runnable() {
public void run() {
data = dbGetData3.getDataFromDB(url);
runOnUiThread(new Runnable() {
#Override
public void run() {
userha = parseJSON3(data);
}
});
}
}).start();
Toast.makeText(context, "please wait ", Toast.LENGTH_LONG)
.show();
} catch (Exception e) {
toast(9);
}
Now i want add a loading progress bar while fetch data finished.
I use AsyncTask like this:
private class LongOperation extends AsyncTask<String, Void, String> {
protected void onPreExecute() {
progressDialog = new ProgressDialog(Login.this);
progressDialog.setTitle("Processing...");
progressDialog.setMessage("Please wait...");
progressDialog.setCancelable(true);
progressDialog.show();
}
protected String doInBackground(String... params) {
try {
get_users();
} catch (Exception e) {
}
return null;
}
protected void onPostExecute(String result) {
progressDialog.dismiss();
}
}
and i use this code for excute
mytask = new LongOperation();
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB)
mytask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else
mytask.execute();
imageView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View arg0) {
onCreate(savedInstanceState);
}
});
but progress dialog dose not show for me (get user worked)
i change my code like this:
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB){
mytask.onPreExecute();
mytask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
else
{
mytask.onPreExecute();
mytask.execute();
}
then my progress dialog allways show
i test other code in stackoverflow like
AsyncTask doInBackground does not run
AsyncTask called from Handler will not execute doInBackground
Android SDK AsyncTask doInBackground not running (subclass)
but that not work for me
please help me tankyou
Consdier using a LoaderManager and an AsyncTaskLoader for this sort of stuff.
AsyncTasks are a pain in the ass as because you have to manage their lifecycle with screen-rotations etc. With a LoaderManager all of that is in the past.
Below is an example of a loader which loads a list of "items".
public class ItemsLoader extends AsyncTaskLoader<List<Item>> {
private static final String TAG = "ItemsLoader";
private List<Item> mItems;
private ItemUpdatedReceiver mObserver;
private int mSomeParam;
public static class ItemUpdatedReceiver extends BroadcastReceiver {
private static final String TAG = "ItemLoader";
final ItemsLoader mLoader;
public ItemUpdatedReceiver(ItemsLoader mLoader) {
this.mLoader = mLoader;
// listen for changes to the account we're using
IntentFilter filter = new IntentFilter(GlobalConstants.ACTION_ITEMS_UPDATED);
LocalBroadcastManager.getInstance(mLoader.getContext()).registerReceiver(this, filter);
}
#Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (GlobalConstants.ACTION_ITEMS_UPDATED.equals(action)) {
mLoader.onContentChanged();
}
}
}
public void setSomeParam(int someParam){
mSomeParam = someParam;
onContentChanged();
}
public ItemsLoader(Context context, int someParam) {
super(context);
mSomeParam = someParam;
onContentChanged();
}
#Override
public List<Item> loadInBackground() {
// do whatever you need to do here
ArrayList<Item> Items = new ArrayList<>();
return Items;
}
/**
* Called when there is new data to deliever to the client.
*
* #param data
*/
#Override
public void deliverResult(List<Item> data) {
if (isReset()) {
// an async query came in while the loader is stopped, we don't need the result
//release resources if needed
onReleaseResources(data);
}
List<Item> oldItems = mItems;
mItems = data;
if (isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(mItems);
}
// At this point we can release the resources associated with
// 'oldApps' if needed; now that the new result is delivered we
// know that it is no longer in use.
if (oldItems != null) {
onReleaseResources(oldItems);
}
}
#Override
protected void onStartLoading() {
super.onStartLoading();
if (mItems != null) {
// If we currently have a result available, deliver it
// immediately.
deliverResult(mItems);
}
// start listening for changes
if (mObserver == null) {
mObserver = new ItemUpdatedReceiver(this);
}
if (takeContentChanged() || mItems == null) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
/**
* Handles a request to stop the Loader.
*/
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
/**
* Handles a request to cancel a load.
*/
#Override
public void onCanceled(List<Item> items) {
super.onCanceled(items);
// At this point we can release the resources associated with 'profile'
// if needed.
onReleaseResources(items);
}
#Override
protected void onReset() {
super.onReset();
// Ensure the laoder is stopped
onStopLoading();
// At this point we can release the resources if needed.
if (mItems != null) {
onReleaseResources(mItems);
mItems = null;
}
// Stop monitoring for changes.
if (mObserver != null) {
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mObserver);
mObserver = null;
}
}
/**
* Helper function to take care of releasing resources associated
* with an actively loaded data set.
*/
private void onReleaseResources(List<Item> data) {
// For a simple List<> there is nothing to do. For something
// like a Cursor, we would close it here.
}
}
To use this class, in your activity you must extend LoaderManager.LoaderCallbacks> and override the methods:
public Loader<List<Item>> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// start the loading dialog here
return new ItemsLoader(context);
}
public void onLoadFinished(Loader<List<Item>> loader, List<Item>data) {
// do something with your data, hide the progress dialog
}
public void onLoaderReset(Loader<Cursor> loader) {
// set the old data to null
}
To actually start loading:
getLoaderManager().initLoader(LOADER_ID, null, this);
I have created simple application with login feature. I have created separate task for do the login into server called LoginTask and a listener class called LoginListener.
public interface LoginListener {
public void onLoginComplete();
public void onLoginFailure(String msg);
}
public class LoginTask extends AsyncTask<String, Void, Boolean>{
private final LoginListener listener;
private final Context c;
private String msg;
public LoginTask(final Context c, final LoginListener listener) {
this.c = c;
this.listener = listener;
}
#Override
protected Boolean doInBackground(String... args) {
// loging in to server
//return true if success
}
#Override
protected void onPostExecute(Boolean status) {
if(!status){
if(listener != null) listener.onLoginFailure(msg);
return;
}
// the problem is here, listener is null, because activity/fragment destroyed
if(listener != null) listener.onLoginComplete();
}
}
I executed LoginTask from LoginFragment. The LoginFragment implements LoginListener.
public class LoginFragment extends Fragment implements LoginListener{
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.frg_login, container, false);
}
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
doInitView();
};
private void doInitView(){
Button loginButton = (Button) getActivity().findViewById(R.id.login_btn);
Button regButton = (Button) getActivity().findViewById(R.id.toreg_btn);
ButtonListener listener = new ButtonListener();
loginButton.setOnClickListener(listener);
regButton.setOnClickListener(listener);
}
private void doLogin(){
Activity activity = getActivity();
EditText emailText = (EditText)activity.findViewById(R.id.login_email);
EditText pwdText = (EditText)activity.findViewById(R.id.login_pwd);
String email = emailText.getText().toString().trim();
String pwd = pwdText.getText().toString().trim();
if(StringUtil.isAnyNull(email, pwd)){
Popup.showMsg(getActivity(), "Silahkan lengkapi data", Popup.SHORT);
return;
}
savedEmail = email;
savedPwd = pwd;
String url = getActivity().getResources().getString(R.string.url_login);
Popup.showLoading(getActivity(), "Login", "Please wait...");
LoginTask task = new LoginTask(getActivity(), this);
task.execute(url, email, pwd);
}
private final class ButtonListener implements OnClickListener{
#Override
public void onClick(View v) {
switch(v.getId()){
case R.id.login_btn:
doLogin();
break;
case R.id.toreg_btn:
doToRegister();
break;
case R.id.demo_btn:
doDemo();
break;
}
}
}
#Override
public void onLoginComplete() {
// getActivity() is null
((MainActivity)getActivity()).gotoMain();
}
#Override
public void onLoginFailure(String msg) {
}
}
Because of the login task takes time, sometime the device light turn off before the task was finished so activity was destroyed. This caused the task failed to call the listener(fragment). How to solve this problem?
Thanks
AsyncTask should be used for tasks that take a bit longer and return a result into the current activity. However it's not intended for really long running tasks or for those cases where you want to evaluate its results even if the activity has been destroyed. You might consider using a Service here. In any case you shouldn't do updates in onPostExecute() anymore cause the activity context might be gone (see Doctoror Drive's post). Having that service in place, you can either send an Intent or a Broadcast event to the system. Then do the further processing in that intent activity / broadcast receiver.
You can cancel the asynctask in onDestroy() of your LoginActivity.
Override onCancelled() of the asynctask. When the activity is destroyed, a call to onCancelled() will be made instead of onPostExecute()
Here you can avoid a call back to the LoginActivity.
You should use Service or IntentService. because AsyncTask does not record any variables or context of Activity. When you finish login task launch PendingIntent or startActivity(intent). This can be best practice of Android. This way you never get exception.
In onLoginComplete and onLoginFailure check if the fragment is still attached to the activity. If not, do nothing.
#Override
public void onLoginComplete() {
if (isAdded() && !isRemoving() && !isDetached()) {
((MainActivity)getActivity()).gotoMain();
}
}
My application has a refresh button on the main activity. When the user presses that button, a new thread is created which starts updating the SQLite database. When this thread started, user could possibly get into another activies of the application.
The problem is these other activities(ListActivity) should be updated according to the DB when that background thread is completed. How could I provide that. I tried getting current task with ActivityManager but It requires extra permission which I dont want.
Edit:
Sorry seems I misunderstood you. Please take a look at the following code, it is similar to Chinaski's (you just use an interface for the callback methods) but I added a bit more to ensure you know how to use it in a way that will avoid memory leaks.
Note how the activity detaches during onDestroy -- alternatively you could use a WeakReference, however these days you'd use a Fragment with setRetainInstance(true) and completely avoid the detaching/attaching as the fragment would be retained.
MyAsyncTask
public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
private Callback mCallback;
private boolean mIsComplete = false;
private boolean mHasCallbackBeenCalled = false;
public MyBackgroundTask(Callback callback) {
mCallback = callback;
}
/** Only safe to call this from the UI thread */
public void attach(Callback callback) {
mCallback = callback;
if (mIsComplete && !mHasCallbackBeenCalled) {
fireCallback();
}
}
/** Only safe to call this from the UI thread */
public void detach() {
mCallback = callback;
}
#Override
public void doInBackground() {
// do the heavy stuff here
return null;
}
#Override
public void onPostExecute(Void result) {
mIsComplete = true;
fireCallback();
}
private void fireCallback() {
if (mCallback != null) {
mCallback.callbackMethod();
mHasCallbackBeenCalled = true;
}
}
public static interface Callback {
public void callbackMethod();
}
}
MyActivity
public class MyActivity extends Activity implements MyAsyncTask.Callback {
private MyAsyncTask mTask;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check for a retained task after a configuration change
// e.g. a rotation
if (getLastNonConfigurationInstance() != null) {
mTask = (MyAsyncTask) getLastNonConfigurationInstance();
// Re-attach the task
mTask.attach(this);
}
}
#Override
public void onDestroy() {
// Detach from task to avoid memory leak
if (mTask != null) {
mTask.detach();
}
super.onDestroy();
}
#Override
public Object onRetainNonConfigurationInstance() {
// Retain the async task duration a rotation
return mTask;
}
/** Callback method */
#Override
public void callbackMethod() {
// Do something here
}
}
You could make a singleton in which you will have your thread and a queue of "tasks". When a task is finished, you check / launch the next task, and when you add a task, you launch it, or add it in the queue if a task is already running.
I don't say this is the best solution, but it's one.
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?