20

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?

dmon
  • 30,048
  • 8
  • 87
  • 96
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • *Old loader will be destroyed* - Is this a problem(the same data will still be available with the new `Loader`, right)? – user Apr 17 '13 at 06:46
  • Same data no longer available. As after the old loader destroyed, its previous loaded data from loadInBackground destroyed as well. Hence, new loader need to run the time consuming loadInBackground again. – Cheok Yan Cheng Apr 17 '13 at 10:09

4 Answers4

6

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();            
    }
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
Kevin Tan
  • 1,038
  • 8
  • 19
  • I think I know retained fragment + asynctask techniques. It is just that, the technique is more cumbersome and more manual work needed. I prefer not to go for that approach, until I realize I need to give up all the goodies of AsyncTaskLoader. Till now, I do not want to give up :) http://stackoverflow.com/questions/15079948/guideline-to-choose-among-asynctaskloader-and-asynctask-to-be-used-in-fragment – Cheok Yan Cheng Apr 17 '13 at 10:17
  • Sometimes manual work gives you more control and better understanding of how everything works. It's been tested and proven working, even from @commonsguy himself. – Kevin Tan Apr 18 '13 at 03:20
  • One of the solution I can think of, is having a retained instance non-ui fragment to hold the data. Anytime before the main fragment attempt to create/re-connect to AsyncTaskLoaders, it will first look for retained instance fragment. If there is, main fragment will not bother on AsyncTaskLoaders. – Cheok Yan Cheng Apr 23 '13 at 18:02
  • If you can provide code example by adding on the top of my existing given code, that will be great. So that there are no objection if I award the 500 points (WOW) to you :) – Cheok Yan Cheng Apr 23 '13 at 18:03
0

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

}    
Mehul Joisar
  • 15,348
  • 6
  • 48
  • 57
  • Not working. If you refer to the above test case `A child activity is launched -> Rotation -> Back button pressed (Wrong)`, restartLoader will never be called. – Cheok Yan Cheng Apr 18 '13 at 13:52
  • ok.what if you change `if ( loader != null && loader.isReset() )` to `if ( loader != null && !loader.isReset() )` – Mehul Joisar Apr 18 '13 at 13:57
0

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.

Sagar Waghmare
  • 4,702
  • 1
  • 19
  • 20
-1

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();
    }
}
Jschools
  • 2,698
  • 1
  • 17
  • 18