Android Fragments. Retaining an AsyncTask during screen rotation or configuration change
R

12

86

I'm working on a Smartphone / Tablet app, using only one APK, and loading resources as is needed depending on screen size, the best design choice seemed to be using Fragments via the ACL.

This app has been working fine until now being only activity based. This is a mock class of how I handle AsyncTasks and ProgressDialogs in the Activities in order to have them work even when the screen is rotated or a configuration change occurs mid communication.

I will not change the manifest to avoid recreation of the Activity, there are many reasons why I dont want to do it, but mainly because the official docs say it isnt recomended and I've managed without it this far, so please dont recomend that route.

public class Login extends Activity {

    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.login);
        //SETUP UI OBJECTS
        restoreAsyncTask();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (pd != null) pd.dismiss();
        if (asyncLoginThread != null) return (asyncLoginThread);
        return super.onRetainNonConfigurationInstance();
    }

    private void restoreAsyncTask();() {
        pd = new ProgressDialog(Login.this);
        if (getLastNonConfigurationInstance() != null) {
            asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
            if (asyncLoginThread != null) {
                if (!(asyncLoginThread.getStatus()
                        .equals(AsyncTask.Status.FINISHED))) {
                    showProgressDialog();
                }
            }
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
        @Override
        protected Boolean doInBackground(String... args) {
            try {
                //Connect to WS, recieve a JSON/XML Response
                //Place it somewhere I can use it.
            } catch (Exception e) {
                return true;
            }
            return true;
        }

        protected void onPostExecute(Boolean result) {
            if (result) {
                pd.dismiss();
                //Handle the response. Either deny entry or launch new Login Succesful Activity
            }
        }
    }
}

This code is working fine, I have around 10.000 users without complaint, so it seemed logical to just copy this logic into the new Fragment Based Design, but, of course, it isnt working.

Here is the LoginFragment:

public class LoginFragment extends Fragment {

    FragmentActivity parentActivity;
    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    public interface OnLoginSuccessfulListener {
        public void onLoginSuccessful(GlobalContainer globalContainer);
    }

    public void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        //Save some stuff for the UI State
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setRetainInstance(true);
        //If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
        parentActivity = getActivity();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
        return loginLayout;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //SETUP UI OBJECTS
        if(savedInstanceState != null){
            //Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
            @Override
            protected Boolean doInBackground(String... args) {
                try {
                    //Connect to WS, recieve a JSON/XML Response
                    //Place it somewhere I can use it.
                } catch (Exception e) {
                    return true;
                }
                return true;
            }

            protected void onPostExecute(Boolean result) {
                if (result) {
                    pd.dismiss();
                    //Handle the response. Either deny entry or launch new Login Succesful Activity
                }
            }
        }
    }
}

I cant use onRetainNonConfigurationInstance() since it has to be called from the Activity and not the Fragment, same goes with getLastNonConfigurationInstance(). I've read some similar questions here with no answer.

I understand that it might require some working around to get this stuff organized properly in fragments, that being said, I would like to maintain the same basic design logic.

What would be the proper way to retain the AsyncTask during a configuration change, and if its still runing, show a progressDialog, taking into consideration that the AsyncTask is a inner class to the Fragment and it is the Fragment itself who invokes the AsyncTask.execute()?

Raffinate answered 7/12, 2011 at 15:29 Comment(3)
Maybe the thread on how to handle configuration change with AsyncTask can helpQuean
associate AsyncTask with an application life-cycle..thus can resume when activity recreatesMcallister
Check out my post on this topic: Handling Configuration Changes with FragmentsCosine
E
75

Fragments can actually make this a lot easier. Just use the method Fragment.setRetainInstance(boolean) to have your fragment instance retained across configuration changes. Note that this is the recommended replacement for Activity.onRetainnonConfigurationInstance() in the docs.

If for some reason you really don't want to use a retained fragment, there are other approaches you can take. Note that each fragment has a unique identifier returned by Fragment.getId(). You can also find out if a fragment is being torn down for a config change through Fragment.getActivity().isChangingConfigurations(). So, at the point where you would decide to stop your AsyncTask (in onStop() or onDestroy() most likely), you could for example check if the configuration is changing and if so stick it in a static SparseArray under the fragment's identifier, and then in your onCreate() or onStart() look to see if you have an AsyncTask in the sparse array available.

Excess answered 18/12, 2011 at 6:57 Comment(6)
Note that the setRetainInstance is only if you are not using back stack.Hotfoot
Isn't it possible that the AsyncTask sends its result back before the onCreateView of the retained Fragment would run?Yacano
@Yacano The lifecycle methods for Activitys, Fragments, etc. are invoked sequentially by the main GUI thread's message queue, so even if the task finished concurrently in the background before these lifecycle methods completed (or even invoked), the onPostExecute method would still need to wait before finally being processed by the main thread's message queue.Cosine
This approach (RetainInstance = true) won't work if you want to load different layout files for each orientation.Kashakashden
Starting the asynctask within the MainActivity's onCreate method only seems to work if the asynctask - inside the "worker" fragment - is started by explicit user action. Because the main thread and user interface are available. Starting the asynctask right after launching the app however - without a user action like a button click - gives an exception. In that case, the asynctask can be called in the onStart method on the MainActivity, not in the onCreate method.Randall
@Justin: Why would this solution not work in that case? On device rotation, the activity is destroyed and a new activity created. If you use the worker fragment solution (androiddesignpatterns.com/2013/04/…), the newly created activity implements the worker fragment and is connected to the asynctask within it. This is not dependent on the layout.Randall
H
66

I think you will enjoy my extremely comprehensive and working example detailed below.

  1. Rotation works, and the dialog survives.
  2. You can cancel the task and dialog by pressing the back button (if you want this behaviour).
  3. It uses fragments.
  4. The layout of the fragment underneath the activity changes properly when the device rotates.
  5. There is a complete source code download and a precompiled APK so you can see if the behaviour is what you want.

Edit

As requested by Brad Larson I have reproduced most of the linked solution below. Also since I posted it I have been pointed to AsyncTaskLoader. I'm not sure it is totally applicable to the same problems, but you should check it out anyway.

Using AsyncTask with progress dialogs and device rotation.

A working solution!

I have finally got everything to work. My code has the following features:

  1. A Fragment whose layout changes with orientation.
  2. An AsyncTask in which you can do some work.
  3. A DialogFragment which shows the progress of the task in a progress bar (not just an indeterminate spinner).
  4. Rotation works without interrupting the task or dismissing the dialog.
  5. The back button dismisses the dialog and cancels the task (you can alter this behaviour fairly easily though).

I don't think that combination of workingness can be found anywhere else.

The basic idea is as follows. There is a MainActivity class which contains a single fragment - MainFragment. MainFragment has different layouts for horizontal and vertical orientation, and setRetainInstance() is false so that the layout can change. This means that when the device orientation is changed, both MainActivity and MainFragment are completely destroyed and recreated.

Separately we have MyTask (extended from AsyncTask) which does all the work. We can't store it in MainFragment because that will be destroyed, and Google has deprecated using anything like setRetainNonInstanceConfiguration(). That isn't always available anyway and is an ugly hack at best. Instead we will store MyTask in another fragment, a DialogFragment called TaskFragment. This fragment will have setRetainInstance() set to true, so as the device rotates this fragment isn't destroyed, and MyTask is retained.

Finally we need to tell the TaskFragment who to inform when it is finished, and we do that using setTargetFragment(<the MainFragment>) when we create it. When the device is rotated and the MainFragment is destroyed and a new instance is created, we use the FragmentManager to find the dialog (based on its tag) and do setTargetFragment(<the new MainFragment>). That's pretty much it.

There were two other things I needed to do: first cancel the task when the dialog is dismissed, and second set the dismiss message to null, otherwise the dialog is weirdly dismissed when the device is rotated.

The code

I won't list the layouts, they are pretty obvious and you can find them in the project download below.

MainActivity

This is pretty straightforward. I added a callback into this activity so it knows when the task is finished, but you might not need that. Mainly I just wanted to show the fragment-activity callback mechanism because it's quite neat and you might not have seen it before.

public class MainActivity extends Activity implements MainFragment.Callbacks
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onTaskFinished()
    {
        // Hooray. A toast to our success.
        Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
        // NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
        // the duration in milliseconds. ANDROID Y U NO ENUM? 
    }
}

MainFragment

It's long but worth it!

public class MainFragment extends Fragment implements OnClickListener
{
    // This code up to onDetach() is all to get easy callbacks to the Activity. 
    private Callbacks mCallbacks = sDummyCallbacks;

    public interface Callbacks
    {
        public void onTaskFinished();
    }
    private static Callbacks sDummyCallbacks = new Callbacks()
    {
        public void onTaskFinished() { }
    };

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
        if (!(activity instanceof Callbacks))
        {
            throw new IllegalStateException("Activity must implement fragment's callbacks.");
        }
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        mCallbacks = sDummyCallbacks;
    }

    // Save a reference to the fragment manager. This is initialised in onCreate().
    private FragmentManager mFM;

    // Code to identify the fragment that is calling onActivityResult(). We don't really need
    // this since we only have one fragment to deal with.
    static final int TASK_FRAGMENT = 0;

    // Tag so we can find the task fragment again, in another instance of this fragment after rotation.
    static final String TASK_FRAGMENT_TAG = "task";

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        // At this point the fragment may have been recreated due to a rotation,
        // and there may be a TaskFragment lying around. So see if we can find it.
        mFM = getFragmentManager();
        // Check to see if we have retained the worker fragment.
        TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);

        if (taskFragment != null)
        {
            // Update the target fragment so it goes to this fragment instead of the old one.
            // This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
            // keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
            // use weak references. To be sure you aren't leaking, you may wish to make your own
            // setTargetFragment() which does.
            taskFragment.setTargetFragment(this, TASK_FRAGMENT);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState)
    {
        super.onViewCreated(view, savedInstanceState);

        // Callback for the "start task" button. I originally used the XML onClick()
        // but it goes to the Activity instead.
        view.findViewById(R.id.taskButton).setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        // We only have one click listener so we know it is the "Start Task" button.

        // We will create a new TaskFragment.
        TaskFragment taskFragment = new TaskFragment();
        // And create a task for it to monitor. In this implementation the taskFragment
        // executes the task, but you could change it so that it is started here.
        taskFragment.setTask(new MyTask());
        // And tell it to call onActivityResult() on this fragment.
        taskFragment.setTargetFragment(this, TASK_FRAGMENT);

        // Show the fragment.
        // I'm not sure which of the following two lines is best to use but this one works well.
        taskFragment.show(mFM, TASK_FRAGMENT_TAG);
//      mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
        {
            // Inform the activity. 
            mCallbacks.onTaskFinished();
        }
    }

TaskFragment

    // This and the other inner class can be in separate files if you like.
    // There's no reason they need to be inner classes other than keeping everything together.
    public static class TaskFragment extends DialogFragment
    {
        // The task we are running.
        MyTask mTask;
        ProgressBar mProgressBar;

        public void setTask(MyTask task)
        {
            mTask = task;

            // Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
            mTask.setFragment(this);
        }

        @Override
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);

            // Retain this instance so it isn't destroyed when MainActivity and
            // MainFragment change configuration.
            setRetainInstance(true);

            // Start the task! You could move this outside this activity if you want.
            if (mTask != null)
                mTask.execute();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
        {
            View view = inflater.inflate(R.layout.fragment_task, container);
            mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);

            getDialog().setTitle("Progress Dialog");

            // If you're doing a long task, you probably don't want people to cancel
            // it just by tapping the screen!
            getDialog().setCanceledOnTouchOutside(false);

            return view;
        }

        // This is to work around what is apparently a bug. If you don't have it
        // here the dialog will be dismissed on rotation, so tell it not to dismiss.
        @Override
        public void onDestroyView()
        {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }

        // Also when we are dismissed we need to cancel the task.
        @Override
        public void onDismiss(DialogInterface dialog)
        {
            super.onDismiss(dialog);
            // If true, the thread is interrupted immediately, which may do bad things.
            // If false, it guarantees a result is never returned (onPostExecute() isn't called)
            // but you have to repeatedly call isCancelled() in your doInBackground()
            // function to check if it should exit. For some tasks that might not be feasible.
            if (mTask != null) {
                mTask.cancel(false);
            }

            // You don't really need this if you don't want.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
        }

        @Override
        public void onResume()
        {
            super.onResume();
            // This is a little hacky, but we will see if the task has finished while we weren't
            // in this activity, and then we can dismiss ourselves.
            if (mTask == null)
                dismiss();
        }

        // This is called by the AsyncTask.
        public void updateProgress(int percent)
        {
            mProgressBar.setProgress(percent);
        }

        // This is also called by the AsyncTask.
        public void taskFinished()
        {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();

            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;

            // Tell the fragment that we are done.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
        }
    }

MyTask

    // This is a fairly standard AsyncTask that does some dummy work.
    public static class MyTask extends AsyncTask<Void, Void, Void>
    {
        TaskFragment mFragment;
        int mProgress = 0;

        void setFragment(TaskFragment fragment)
        {
            mFragment = fragment;
        }

        @Override
        protected Void doInBackground(Void... params)
        {
            // Do some longish task. This should be a task that we don't really
            // care about continuing
            // if the user exits the app.
            // Examples of these things:
            // * Logging in to an app.
            // * Downloading something for the user to view.
            // * Calculating something for the user to view.
            // Examples of where you should probably use a service instead:
            // * Downloading files for the user to save (like the browser does).
            // * Sending messages to people.
            // * Uploading data to a server.
            for (int i = 0; i < 10; i++)
            {
                // Check if this has been cancelled, e.g. when the dialog is dismissed.
                if (isCancelled())
                    return null;

                SystemClock.sleep(500);
                mProgress = i * 10;
                publishProgress();
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Void... unused)
        {
            if (mFragment == null)
                return;
            mFragment.updateProgress(mProgress);
        }

        @Override
        protected void onPostExecute(Void unused)
        {
            if (mFragment == null)
                return;
            mFragment.taskFinished();
        }
    }
}

Download the example project

Here is the source code and the APK. Sorry, the ADT insisted on adding the support library before it would let me make a project. I'm sure you can remove it.

Hypertensive answered 6/9, 2012 at 15:39 Comment(20)
I would avoid retaining the progress bar DialogFragment , because it has UI elements which hold references to the old context. Instead, I'd store AsyncTask in another empty fragment, and set DialogFragment as its target.Toplofty
Won't those references be cleared when the device is rotated and onCreateView() is called again? The old mProgressBar will be overwritten with a new one at least.Hypertensive
Not explicitly, but I'm pretty sure about it. You can add mProgressBar = null; in onDestroyView() if you want to be extra sure. Singularity's method may be a good idea, but it will increase the code complexity even more!Hypertensive
The reference you hold on the asynctask is the progressdialog fragment, right? so 2 questions: 1- what if i want to alter the real fragment that calls the progressdialog; 2- what if i want to pass params to the asynctask? Regards,Stradivarius
Also will this work if the mainfragment is inside a viewPager?Stradivarius
@Maxrunner, For passing parameters, the easiest thing is probably to move mTask.execute() to MainFragment.onClick(). Alternatively you could allow parameters to be passed in setTask() or even store them in MyTask itself. I'm not exactly sure what your first question means, but maybe you want to use TaskFragment.getTargetFragment()? I'm pretty sure it will work using a ViewPager. But ViewPagers aren't very well understood or documented, so good luck! Remember your fragment isn't created until it is visible the first time.Hypertensive
Hi there Timmmm. I was talking about changing the layout of the MainFragment after finishing the task, because The asynctask MyTask has a reference to the TaskFragment DialogFragment, not the MainFragment. But i think i can study this a bit more. The more important thing is the execute. You're saying i can execute the task in the MainFragment, right? So in the TaskFragment dialog in the onCreate() method i can remove the line : IF(mTask!=null) mTask.execute() correct? regards,Stradivarius
Also if i need the setRetainInstance on the mainfragment im getting the error: IllegalStateException: Can not perform this action after onSaveInstanceState after i rotate and press the button again. Im using the support package v4. Goona try using the latest apis.Stradivarius
I'm not really an expert, but you can hack around the IllegalStateException by using commitAllowingStateLoss() probably (see the line where I have two alternatives - commit() or show()). You might want to ask your questions as a new question - then you can post your code.Hypertensive
@Timmmm: What if I have several AsyncTask in my MainActivity.java file? These will be executed individually. e.g. One AsyncTask is to download, another for something else work, and so on. How can I use your code in such a situation?Cori
I also wonder about the same thing as @Cori mentioned. Perhaps one solution would be to have a Task interface and implement it by the asynctasks in your project. then have a reference to a Task object in the TaskFragment. But I'm not sure.Withe
Been checking this, but where do we call the task cancel to stop doing the background work?Stradivarius
The links are gone. The code provided here worked for me though. I believe it's the most comprehensive solution to the problem. Oh, I changed one line - I'm getting the Manager this way: mFM = getActivity().getSupportFragmentManager()Carolinian
Sorry, the website will return soon, but this is actually the most up to date code anyway so you aren't really missing anything!Hypertensive
@Maxrunner: This may no longer be required but I was facing a similar issue with regards to communicating between different fragments, specifically communicating back to the MainFragment. With reference to Android's guidelines regarding the need for communication between fragments to be managed by the calling Activity; I used this example in conjunction with Alex Lockwoods below, whereby the AsyncTask's progress/results is returned to it's Activity. The Activity then manages the feedback to the MainFragment by means of a fragment manager.Pekan
Waiting for the link to be back, running into the same problem now :(Loos
Sorry, life etc. I'll try to do it tonight!Hypertensive
I updated the APK and source links. Note this code is now really quite old and I haven't followed Android development for a while. There may be better ways to do it now!Hypertensive
@Timmmm, If my application is following a single activity - multiple fragments design, should I have a separate task fragment for each of my UI fragments? So basically each task fragment's lifecycle will be managed by its target fragment, and there would be no communication with the activity.Deletion
My app has been using a UI-less task fragment architecture to great effect, but recently I am getting crashes on Resume of many activities - but only on Oreo devices. I am struggling to figure out if there is an OS bug introduced in Oreo that would destroy UI-less Fragments but not change the Fragment count. Has anyone else using this architecture started getting similar bugs?I will probably open a separate question. Unable to resume activity {com.my.activity.MainNavActivity}: java.lang.IndexOutOfBoundsException: Index: 3, Size: 3Lambeth
C
16

I've recently posted an article describing how to handle configuration changes using retained Fragments. It solves the problem of retaining an AsyncTask across a rotation change nicely.

The TL;DR is to use host your AsyncTask inside a Fragment, call setRetainInstance(true) on the Fragment, and report the AsyncTask's progress/results back to it's Activity (or it's target Fragment, if you choose to use the approach described by @Timmmm) through the retained Fragment.

Cosine answered 30/4, 2013 at 16:46 Comment(8)
How would You go about the nested Fragments? Like an AsyncTask started from a RetainedFragment inside another Fragment (Tab).Carolinian
If the fragment is already retained, then why not just execute the async task from within that retained fragment? If it's already retained then the async task will be able to report back to it even if a configuration change occurs.Cosine
@AlexLockwood Thanks for the blog. Instead of having to handle onAttach and onDetach, will it be better if inside TaskFragment, we just call getActivity whenever we need to fire a callback. (By checking instaceof TaskCallbacks)Politicking
@CheokYanCheng What would be the advantage of doing it that way? It sounds like the same thing... just less lines of code. :)Cosine
@AlexLockwood Yes. Lesser line of code and lesser methods (No need override onAttach & onDetach) to maintain. I thought you might have other reasons not using getActivity? As when I refer to Google official example developer.android.com/guide/components/fragments.html , they also been using yours way.Politicking
The reason I ask so, as all my legacy code are using getActivity. So, I thought I might be doing some incorrect stuff, after looking at your code...Politicking
You can do it either way. I just did it in onAttach() and onDetach() so I could avoid constantly casting the activity to TaskCallbacks every time I wanted to use it.Cosine
@AlexLockwood If my application is following a single activity - multiple fragments design, should I have a separate task fragment for each of my UI fragments? So basically each task fragment's lifecycle will be managed by its target fragment, and there would be no communication with the activity.Deletion
K
13

My first suggestion is to avoid inner AsyncTasks, you can read a question that I asked about this and the answers: Android: AsyncTask recommendations: private class or public class?

After that i started using non-inner and... now i see A LOT of benefits.

The second is, keep a reference of your running AsyncTask in the Application Class - http://developer.android.com/reference/android/app/Application.html

Everytime you start an AsyncTask, set it on the Application and when it finishes it set it to null.

When a fragment/activity starts you can check if any AsyncTask is running (by checking if it's null or not on the Application) and then set the reference inside to whatever you want (activity, fragment etc so you can do callbacks).

This will solve your problem: If you only have 1 AsyncTask running at any determined time you can add a simple reference:

AsyncTask<?,?,?> asyncTask = null;

Else, have in the Aplication a HashMap with references to them.

The progress dialog can follow the exact same principle.

Kaif answered 15/12, 2011 at 14:41 Comment(10)
I agreed as long as you bind AsyncTask's life cycle to its Parent (by defining AsyncTask as an inner class of Activity/Fragment), it is fairly hard to make your AsyncTask escape from its parent's lifecycle recreation, however, I don't like your solution, it looks just so hacky.Pantomime
The question is.. do you have a better solution?Kaif
I'm going to have to agree with @Pantomime here, this solution aws presented to me a while ago when I was dealing with this issue without using fragments (Activity based app). This question: #2621417 has the same answer, and I agree with one of the comments that says "Application instance has its own life cycle - it can be killed by OS too, so this solution may cause a hard-to-reproduce bug"Raffinate
Still i don't see any other way that is less "hacky" as @Pantomime said. I'm been using it in several apps, and with some attention to the possible problems it all works great.Kaif
Maybe the @Excess solution suites you more.Kaif
I agree with @Pantomime not to use AsyncTask.Quean
@NeTeInStEiN yes, there is a better solution: IntentServiceQuean
Just completely disagreeing with all the comments. I think this is the less hacky more clean solution to the problem. :)Broussard
How can you work with "external" asynctask when you have to change the fragment ui on the finish of the doInBackground? You have always to bring the fragment references?Toodleoo
As said above "When a fragment/activity starts you can check if any AsyncTask is running (by checking if it's null or not on the Application) and then set the reference inside to whatever you want (activity, fragment etc so you can do callbacks)."Kaif
E
4

I came up with a method of using AsyncTaskLoaders for this. It's pretty easy to use and requires less overhead IMO..

Basically you create an AsyncTaskLoader like this:

public class MyAsyncTaskLoader extends AsyncTaskLoader {
    Result mResult;
    public HttpAsyncTaskLoader(Context context) {
        super(context);
    }

    protected void onStartLoading() {
        super.onStartLoading();
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() ||  mResult == null) {
            forceLoad();
        }
    }

    @Override
    public Result loadInBackground() {
        SystemClock.sleep(500);
        mResult = new Result();
        return mResult;
    }
}

Then in your activity that uses the above AsyncTaskLoader when a button is clicked:

public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {

    private String username,password;       
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mylayout);
        //this is only used to reconnect to the loader if it already started
        //before the orientation changed
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    public void doBackgroundWorkOnClick(View button) {
        //might want to disable the button while you are doing work
        //to prevent user from pressing it again.

        //Call resetLoader because calling initLoader will return
        //the previous result if there was one and we may want to do new work
        //each time
        getSupportLoaderManager().resetLoader(0, null, this);
    }   


    @Override
    public Loader<Result> onCreateLoader(int i, Bundle bundle) {
        //might want to start a progress bar
        return new MyAsyncTaskLoader(this);
    }


    @Override
    public void onLoadFinished(Loader<LoginResponse> loginLoader,
                               LoginResponse loginResponse)
    {
        //handle result
    }

    @Override
    public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
    {
        //remove references to previous loader resources

    }
}

This seems to handle orientation changes fine and your background task will continue during the rotation.

A few things to note:

  1. If in onCreate you reattach to the asynctaskloader you will get called back in onLoadFinished() with the previous result (even if you had already been told the request was complete). This is actually good behavior most of the time but sometimes it can be tricky to handle. While I imagine there are lots of ways to handle this what I did was I called loader.abandon() in onLoadFinished. Then I added check in onCreate to only reattach to the loader if it wasn't already abandoned. If you need the resulting data again you won't want to do that. In most cases you want the data.

I have more details on using this for http calls here

Ent answered 3/11, 2013 at 8:15 Comment(2)
Sure that getSupportLoaderManager().getLoader(0); won't return null (because the loader with that id, 0, doesn't exist yet)?Soothfast
Yeah it'll be null unless a configuration change caused the activity to restart while the loader was in progress.. That's why I had the check for null.Ent
H
3

I created a very tiny open-source background task library which is heavily based on the Marshmallow AsyncTask but with additional functionality such as:

  1. Automatically retaining tasks across configuration changes;
  2. UI callback (listeners);
  3. Doesn't restart or cancel task when the device rotates (like Loaders would do);

The library internally uses a Fragment without any user interface, which is retained accross configuration changes (setRetainInstance(true)).

You can find it on GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

Most basic example (version 0.2.0):

This example fully retains the task, using a very limited amount of code.

Task:

private class ExampleTask extends Task<Integer, String> {

    public ExampleTask(String tag){
        super(tag);
    }

    protected String doInBackground() {
        for(int i = 0; i < 100; i++) {
            if(isCancelled()){
                break;
            }
            SystemClock.sleep(50);
            publishProgress(i);
        }
        return "Result";
    }
}

Activity:

public class Main extends TaskActivityCompat implements Task.Callback {

    @Override
    public void onClick(View view){
        ExampleTask task = new ExampleTask("activity-unique-tag");
        getTaskManager().execute(task, this);
    }

    @Override
    public Task.Callback onPreAttach(Task<?, ?> task) {
        //Restore the user-interface based on the tasks state
        return this; //This Activity implements Task.Callback
    }

    @Override
    public void onPreExecute(Task<?, ?> task) {
        //Task started
    }

    @Override
    public void onPostExecute(Task<?, ?> task) {
        //Task finished
        Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
    }
}
Hawkins answered 5/3, 2016 at 1:8 Comment(0)
P
1

My approach is to use delegation design pattern, in general, we can isolate the actual business logic (read data from internet or database or whatsoever) from AsyncTask (the delegator) to BusinessDAO (the delegate), in your AysncTask.doInBackground() method, delegate the actual task to BusinessDAO, then implement a singleton process mechanism in BusinessDAO, so that multiple call to BusinessDAO.doSomething() will just trigger one actual task running each time and waiting for the task result. The idea is retain the delegate (i.e. BusinessDAO) during the configuration change, instead of the delegator (i.e. AsyncTask).

  1. Create/Implement our own Application, the purpose is to create/initialize BusinessDAO here, so that our BusinessDAO's lifecycle is application scoped, not activity scoped, note that you need change AndroidManifest.xml to use MyApplication:

    public class MyApplication extends android.app.Application {
      private BusinessDAO businessDAO;
    
      @Override
      public void onCreate() {
        super.onCreate();
        businessDAO = new BusinessDAO();
      }
    
      pubilc BusinessDAO getBusinessDAO() {
        return businessDAO;
      }
    
    }
    
  2. Our existing Activity/Fragement are mostly unchanged, still implement AsyncTask as an inner class and involve AsyncTask.execute() from Activity/Fragement, the difference now is AsyncTask will delegate the actual task to BusinessDAO, so during the configuration change, a second AsyncTask will be initialized and executed, and call BusinessDAO.doSomething() second time, however, second call to BusinessDAO.doSomething() will not trigger a new running task, instead, waiting for current running task to finish:

    public class LoginFragment extends Fragment {
      ... ...
    
      public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {
        // get a reference of BusinessDAO from application scope.
        BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO();
    
        @Override
        protected Boolean doInBackground(String... args) {
            businessDAO.doSomething();
            return true;
        }
    
        protected void onPostExecute(Boolean result) {
          //Handle task result and update UI stuff.
        }
      }
    
      ... ...
    }
    
  3. Inside BusinessDAO, implement singleton process mechanism, for example:

    public class BusinessDAO {
      ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1));
      Future<MyTask> myFutureTask = null;
    
      public void doSomething() {
        if (myFutureTask == null) {
          // nothing running at the moment, submit a new callable task to run.
          MyTask myTask = new MyTask();
          myFutureTask = completionExecutor.submit(myTask);
        }
        // Task already submitted and running, waiting for the running task to finish.
        myFutureTask.get();
      }
    
      // If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception.
      private class MyTask extends Callable<MyTask> {
        public MyAsyncTask call() {
          // do your job here.
          return this;
        }
      }
    
    }
    

I am not 100% sure if this will work, moreover, the sample code snippet should be considered as pseudocode. I am just trying to give you some clue from design level. Any feedback or suggestions are welcome and appreciated.

Pantomime answered 16/12, 2011 at 22:16 Comment(1)
Seems very nice solution. Since you answered this about 2 and half years ago did you tested it since?! You are saying i'm not sure it's working, the thing is neither do I!! I'm locking for a well tested solution for this problem. Do you have any suggestions?Falzetta
A
1

You could make the AsyncTask a static field. If you need a context, you should ship your application context. This will avoid memory leaks, otherwise you'd keep a reference to your entire activity.

Antebellum answered 17/12, 2011 at 18:41 Comment(0)
G
1

If anyone finds their way to this thread then I found a clean approach was to run the Async task from an app.Service (started with START_STICKY) and then on recreate iterate over the running services to find out whether the service (and hence async task) is still running;

    public boolean isServiceRunning(String serviceClassName) {
    final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
    final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

    for (RunningServiceInfo runningServiceInfo : services) {
        if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
            return true;
        }
    }
    return false;
 }

If it is, re-add the DialogFragment (or whatever) and if it is not ensure the dialog has been dismissed.

This is particularly pertinent if you are using the v4.support.* libraries since (at the time of writing) they have know issues with the setRetainInstance method and view paging. Furthermore, by not retaining the instance you can recreate your activity using a different set of resources (i.e. a different view layout for the new orientation)

Glottal answered 24/7, 2012 at 10:16 Comment(4)
Isn't it overkill to run a Service just to retain an AsyncTask? A Service runs in its own process, and that doesn't come without additional costs.Mauricemauricio
Interesting Vinay. Haven't noticed the app being any more resource heavy (it's pretty lightweight anyway at the moment). What have you found? I view a service as a predictable environment for letting the system get on with some heavy lifting or I/O regardless of the UI state. Communicating with the service to see when something has completed seemed 'right'. The services I start to execute a bit of work stop on the task's completion so typically survive around 10-30s.Glottal
Commonsware's answer here seems to suggest Services are a bad idea. I'm now considering AsyncTaskLoaders but they seem to come with issues of their own (inflexibility, only for data loading etc)Mauricemauricio
I see. Noticeably, this service you linked to was explicitly being set-up to run in its own process. The master didn't appear to like this pattern being used often. I haven't explicitly provided those "run in a new process each time" properties so hopefully I am insulated from that portion of criticism. I will endeavour to quantify the effects. Services as a concept of course are not 'bad ideas' and are fundamental to every app doing anything remotely interesting, no pun intended. Their JDoc provides more guidance on their use if you are still not sure.Glottal
C
0

I write samepl code to solve this problem

First step is make Application class:

public class TheApp extends Application {

private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();

@Override
public void onCreate() {
    super.onCreate();
    sTheApp = this;
}

public static TheApp get() {
    return sTheApp;
}

public void registerTask(String tag, AsyncTask<?,?,?> task) {
    tasks.put(tag, task);
}

public void unregisterTask(String tag) {
    tasks.remove(tag);
}

public AsyncTask<?,?,?> getTask(String tag) {
    return tasks.get(tag);
}
}

In AndroidManifest.xml

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="com.example.tasktest.TheApp">

Code in activity:

public class MainActivity extends Activity {

private Task1 mTask1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTask1 = (Task1)TheApp.get().getTask("task1");

}

/*
 * start task is not running jet
 */
public void handletask1(View v) {
    if (mTask1 == null) {
        mTask1 = new Task1();
        TheApp.get().registerTask("task1", mTask1);
        mTask1.execute();
    } else
        Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();

}

/*
 * cancel task if is not finished
 */
public void handelCancel(View v) {
    if (mTask1 != null)
        mTask1.cancel(false);
}

public class Task1 extends AsyncTask<Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {
        try {
            for(int i=0; i<120; i++) {
                Thread.sleep(1000);
                Log.i("tests", "loop=" + i);
                if (this.isCancelled()) {
                    Log.e("tests", "tssk cancelled");
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onCancelled(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }

    @Override
    protected void onPostExecute(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }
}

}

When activity orientation changes variable mTask is inited from app context. When task is finished variable is set to null and remove from memory.

For me its enough.

Cogswell answered 3/12, 2013 at 20:41 Comment(0)
G
0

Have a look at below example , how to use retained fragment to retain background task:

public class NetworkRequestFragment extends Fragment {

    // Declare some sort of interface that your AsyncTask will use to communicate with the Activity
    public interface NetworkRequestListener {
        void onRequestStarted();
        void onRequestProgressUpdate(int progress);
        void onRequestFinished(SomeObject result);
    }

    private NetworkTask mTask;
    private NetworkRequestListener mListener;

    private SomeObject mResult;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Try to use the Activity as a listener
        if (activity instanceof NetworkRequestListener) {
            mListener = (NetworkRequestListener) activity;
        } else {
            // You can decide if you want to mandate that the Activity implements your callback interface
            // in which case you should throw an exception if it doesn't:
            throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
            // or you could just swallow it and allow a state where nobody is listening
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Retain this Fragment so that it will not be destroyed when an orientation
        // change happens and we can keep our AsyncTask running
        setRetainInstance(true);
    }

    /**
     * The Activity can call this when it wants to start the task
     */
    public void startTask(String url) {
        mTask = new NetworkTask(url);
        mTask.execute();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // If the AsyncTask finished when we didn't have a listener we can
        // deliver the result here
        if ((mResult != null) && (mListener != null)) {
            mListener.onRequestFinished(mResult);
            mResult = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // We still have to cancel the task in onDestroy because if the user exits the app or
        // finishes the Activity, we don't want the task to keep running
        // Since we are retaining the Fragment, onDestroy won't be called for an orientation change
        // so this won't affect our ability to keep the task running when the user rotates the device
        if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
            mTask.cancel(true);
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();

        // This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
        // When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
        // we don't want to keep any references to it
        // When the Activity is being re-created, onAttach will be called and we will get our listener back
        mListener = null;
    }

    private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {

        @Override
        protected void onPreExecute() {
            if (mListener != null) {
                mListener.onRequestStarted();
            }
        }

        @Override
        protected SomeObject doInBackground(String... urls) {
           // Make the network request
           ...
           // Whenever we want to update our progress:
           publishProgress(progress);
           ...
           return result;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mListener != null) {
                mListener.onRequestProgressUpdate(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(SomeObject result) {
            if (mListener != null) {
                mListener.onRequestFinished(result);
            } else {
                // If the task finishes while the orientation change is happening and while
                // the Fragment is not attached to an Activity, our mListener might be null
                // If you need to make sure that the result eventually gets to the Activity
                // you could save the result here, then in onActivityCreated you can pass it back
                // to the Activity
                mResult = result;
            }
        }

    }
}
Grote answered 15/7, 2017 at 15:17 Comment(0)
S
-1

Have a look here.

There is a solution based on Timmmm's solution.

But I improved it:

  • Now the solution is extendable - you only need to extend FragmentAbleToStartTask

  • You able to keep running several tasks at the same time.

    And in my opinion it's as easy as startActivityForResult and receive result

  • You also can stop a running task and check whether particular task is running

Sorry for my English

Sac answered 24/4, 2014 at 14:6 Comment(1)
first link is brokenSchacker

© 2022 - 2024 — McMap. All rights reserved.