Best practice: AsyncTask during orientation change
Asked Answered
I

9

153

AsyncTask is a great thing to run complex tasks in another thread.

But when there is an orientation change or another configuration change while the AsyncTask is still running, the current Activity is destroyed and restarted. And as the instance of AsyncTask is connected to that activity, it fails and causes a "force close" message window.

So, I am looking for some kind of "best-practice" to avoid these errors and prevent AsyncTask from failing.

What I've seen so far is:

  • Disable orientation changes.(For sure not the way you should handle this.)
  • Letting the task survive and updating it with the new activity instance via onRetainNonConfigurationInstance
  • Just canceling the task when the Activity is destroyed and restarting it when the Activity is created again.
  • Binding the task to the application class instead of the activity instance.
  • Some method used in the "shelves" project (via onRestoreInstanceState)

Some code examples:

Android AsyncTasks during a screen rotation, Part I and Part II

ShelvesActivity.java

Can you help me to find the best approach which solves the problem best and is easy to implement as well? The code itself is also important as I don't know how to solve this correctly.

Islington answered 20/8, 2011 at 0:18 Comment(4)
There is a duplicate, check this #4584515.Fairhaired
This is from Mark Murphy's Blog...AsyncTask and ScreenRotation might help...linkAccomplice
Although this is an old post, but this IMO, is a much easier (and better?) approach.Speculate
Im just wondering why isn't the documentation speaking about such very trivial situations.Chaffer
A
141

Do NOT use android:configChanges to address this issue. This is very bad practice.

Do NOT use Activity#onRetainNonConfigurationInstance() either. This is less modular and not well-suited for Fragment-based applications.

You can read my article describing how to handle configuration changes using retained Fragments. It solves the problem of retaining an AsyncTask across a rotation change nicely. You basically need to host your AsyncTask inside a Fragment, call setRetainInstance(true) on the Fragment, and report the AsyncTask's progress/results back to it's Activity through the retained Fragment.

Approval answered 30/4, 2013 at 16:53 Comment(24)
Nice idea, but not everyone uses Fragments. There's a lot of legacy code written long before Fragments were an option.Require
@ScottBiggs Fragments are available via the support library all the way back to Android 1.6. And could you give an example of some legacy code that is still actively being used that would have trouble making use of the support library Fragments? Because I honestly don't think that's an issue.Approval
@AlexLockwood It is an issue if your legacy code use TabActivity as the root activity. First TabActivity cannot contain fragments, second even if it's child fragment inherit from FragmentActivty you still cannot use it because it's not the top level activity.Cimino
@Cimino I didn't feel the need to address these issues in my answer, as 99.9% of people no longer use TabActivity. To be honest, I'm not sure why we are even talking about this... everyone agrees that Fragments are the way to go. :)Approval
What if the AsyncTask must be called from a nested Fragment?Bradbury
@Eddnav If you read my article, there is a link to a Github repo with sample code. Does that sample code address your question? I'm not exactly sure what you mean by a "nested" fragment.Approval
Please, ca you help me ? #28080995Hideaway
@AlexLockwood Do you know if it is possible for the AsyncTask to return at an inopportune moment (halfway through the rotate, when neither activity object exists)?Kerrin
@Kerrin I think the question you are asking is discussed here: https://mcmap.net/q/159987/-android-ui-thread-message-queue-dispatch-order/844882Approval
@AlexLockwood I've actually just come up against a problem using this pattern alongside dialogs - unfortunately onDestroy can be too late on to remove callbacks to an app. I found someone who commented on your post with the same concern: androiddesignpatterns.com/2013/04/…Kerrin
@Kerrin Committing transactions in asynchronous callbacks like onPostExecute() is a different problem entirely. :) I've replied to the comment you linked to in your comment, hopefully it helps. In case you haven't seen it, I've also written a post about the onSaveInstanceState/activity state loss/FragmentTransactions issue here.Approval
thanks again Alex. is it really just about whether you are committing fragment transactions or is there a wider problem about editing the UI? OnSaveInstanceState saves everything about your UI after-all... What if instead of a dialog, i had some text saying "loading..."? I'd have the same problem right? I found your comment about PauseHandlerin the article you linked. It seems this might be the real solution to go for here - set "paused" in onsaveinstancestate if "IsChangingConfiguration" is true. I've got to say this does feel like the rough edges in the android framework...Kerrin
Note, I also tried to cancel connections to the task in the retained fragment's onSaveInstanceState method, but this is unfortunately called after the activity's meaning the behaviour can't be hidden in the retained fragment either. (a taskCancelled callback goes back to the activity in this design and ends up causing state change in the activity)Kerrin
@Kerrin Maybe you should ask a new question on SO with more specific details of the problem that you are encountering since this comment thread is getting pretty lengthy as is. Feel free to leave a link to the post if you create one and I can take a look.Approval
done #28683776Kerrin
@AlexLockwood - "everyone agrees that Fragments are the way to go.". The Devs over at Squared would disagree!Peter
@Peter Square's article advocating against fragments talked mostly about how they prefer using custom views for multipane layouts and backstack management instead. They don't really talk about whether they recommend using fragments to retain objects across config changes, which is what this question is asking... so I guess we'll never know. :)Approval
Using Stack Overflow to spam links to your own product via disguised links is a little below the belt, no?Levey
@AlexLockwood I can see so many flaws and loopholes in this answer. For example, how (or when) would you "re-inflate" the fragment's layout when you have different layouts for different screen sizes, rotation and framework versions? setRetainInstance(true) changes the lifecycle of the fragment and onCreate is not called. And yeah, NOT everyone uses fragments ALL the time...Epispastic
Good article. As for deprecated function "onRetainNonConfigurationInstance()" there is also "onRetainCustomNonConfigurationInstance()" and "getLastCustomNonConfigurationInstance()" from android.support.v4.app.FragmentActivity. They also can be used.Porcupine
I have read the official tutorial on Android right now, and there were much nothing about obligatory using of fragments. So far, downvoted this answer.Caulk
I'm using this approach and at this time all is ok. Thanks & upvoteEthanol
@AlexLockwood The problem here is that you cannot use UI ProgressBar in AsyncTask class because in your example that is marked as static. As I need to hide/show (hide button, show progress) some UIs on onPreExecute and onPostExecute. Currently it seems that I have to pass UI components to AsyncTask's constructor.Muchness
@AlexLockwood In the worst case what could a stateLoss mean? Can you please provide some idea on this?Chaffer
G
36

I usually solve this by having my AsyncTasks fire broadcast Intents in the .onPostExecute() callback, so they don't modify the Activity that started them directly. The Activities listen to these broadcasts with dynamic BroadcastReceivers and act accordingly.

This way the AsyncTasks don't have to care about the specific Activity instance that handles their result. They just "shout" when they're finished, and if an Activity is around that time (is active and focused / is in it's resumed state) which is interested in the results of the task, then it will be handled.

This involves a bit more overhead, since the runtime needs to handle the broadcast, but I usually don't mind. I think using the LocalBroadcastManager instead of the default system wide one speeds things up a bit.

Grapery answered 5/11, 2012 at 9:1 Comment(4)
if u can add a example to the answer it would be more helpfulHummock
I think this is the solution that offers less coupling between activities and fragmentsPadrone
This might be part of a solution but it doesn't seem like it would solve the problem of the AsyncTask getting recreated after the orientation change.Stain
What if you are unlucky and neither activity is around during the broadcast? (i.e. you are mid-rotate)Kerrin
P
24

Here is another example of an AsyncTask that uses a Fragment to handle runtime configuration changes (as when the user rotates the screen) with setRetainInstance(true). A determinate (regularly updated) progress bar is also demonstrated.

The example is partly based on the official docs, Retaining an Object During a Configuration Change.

In this example the work requiring a background thread is the mere loading of an image from the internet into the UI.

Alex Lockwood appears to be right that when it comes to handling runtime configuration changes with AsyncTasks using a "Retained Fragment" is best practice. onRetainNonConfigurationInstance() gets deprecated in Lint, in Android Studio. The official docs warn us off using android:configChanges, from Handling the Configuration Change Yourself, ...

Handling the configuration change yourself can make it much more difficult to use alternative resources, because the system does not automatically apply them for you. This technique should be considered a last resort when you must avoid restarts due to a configuration change and is not recommended for most applications.

Then there is the issue of whether one should use an AsyncTask at all for the background thread.

The official reference for AsyncTask warns ...

AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent pacakge such as Executor, ThreadPoolExecutor and FutureTask.

Alternatively one could use a service, loader (using a CursorLoader or AsyncTaskLoader), or content provider to perform asynchronous operations.

I break the rest of the post into:

  • The Procedure; and
  • All the code for the above procedure.

The Procedure

  1. Start with a basic AsyncTask as an inner class of an activity (it doesn't need to be an inner class but it will probably be convenient to be). At this stage the AsyncTask does not handle runtime configuration changes.

    public class ThreadsActivity extends ActionBarActivity {
    
        private ImageView mPictureImageView;
    
        private class LoadImageFromNetworkAsyncTask
                              extends AsyncTask<String, Void, Bitmap> {
    
            @Override
            protected Bitmap doInBackground(String... urls) {
                return loadImageFromNetwork(urls[0]);
            }
    
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                mPictureImageView.setImageBitmap(bitmap);
            }
        }
    
        /**
         * Requires in AndroidManifext.xml
         *  <uses-permission android:name="android.permission.INTERNET" />
         */
        private Bitmap loadImageFromNetwork(String url) {
            Bitmap bitmap = null;
            try {
                bitmap = BitmapFactory.decodeStream((InputStream)
                                              new URL(url).getContent());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_threads);
    
            mPictureImageView =
                (ImageView) findViewById(R.id.imageView_picture);
        }
    
        public void getPicture(View view) {
            new LoadImageFromNetworkAsyncTask()
                .execute("http://i.imgur.com/SikTbWe.jpg");
        }
    
    }
    
  2. Add a nested class RetainedFragment that extends the Fragement class and doesn't have it's own UI. Add setRetainInstance(true) to the onCreate event of this Fragment. Provide procedures to set and get your data.

    public class ThreadsActivity extends Activity {
    
        private ImageView mPictureImageView;
        private RetainedFragment mRetainedFragment = null;
        ...
    
        public static class RetainedFragment extends Fragment {
    
            private Bitmap mBitmap;
    
            @Override
            public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
    
                // The key to making data survive
                // runtime configuration changes.
                setRetainInstance(true);
            }
    
            public Bitmap getData() {
                return this.mBitmap;
            }
    
            public void setData(Bitmap bitmapToRetain) {
                this.mBitmap = bitmapToRetain;
            }
        }
    
        private class LoadImageFromNetworkAsyncTask
                        extends AsyncTask<String, Integer,Bitmap> {
        ....
    
  3. In the outermost Activity class's onCreate() handle the RetainedFragment: Reference it if it already exists (in case the Activity is restarting); create and add it if it doesn't exist; Then, if it already existed, get data from the RetainedFragment and set your UI with that data.

    public class ThreadsActivity extends Activity {
    
        ...
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_threads);
    
            final String retainedFragmentTag = "RetainedFragmentTag";
    
            mPictureImageView =
                      (ImageView) findViewById(R.id.imageView_picture);
            mLoadingProgressBar =
                    (ProgressBar) findViewById(R.id.progressBar_loading);
    
            // Find the RetainedFragment on Activity restarts
            FragmentManager fm = getFragmentManager();
            // The RetainedFragment has no UI so we must
            // reference it with a tag.
            mRetainedFragment =
              (RetainedFragment) fm.findFragmentByTag(retainedFragmentTag);
    
            // if Retained Fragment doesn't exist create and add it.
            if (mRetainedFragment == null) {
    
                // Add the fragment
                mRetainedFragment = new RetainedFragment();
                fm.beginTransaction()
                    .add(mRetainedFragment, retainedFragmentTag).commit();
    
            // The Retained Fragment exists
            } else {
    
                mPictureImageView
                    .setImageBitmap(mRetainedFragment.getData());
            }
        }
    
  4. Initiate the AsyncTask from the UI

    public void getPicture(View view) {
        new LoadImageFromNetworkAsyncTask().execute(
                "http://i.imgur.com/SikTbWe.jpg");
    }
    
  5. Add and code a determinate progress bar:

    • Add a progress bar to the UI layout;
    • Get a reference to it in the Activity oncreate();
    • Make it visible and invisble at the start and end of the process;
    • Define the progress to report to UI in onProgressUpdate.
    • Change the AsyncTask 2nd Generic parameter from Void to a type that can handle progress updates (e.g. Integer).
    • publishProgress at regular points in doInBackground().

All the code for the above procedure

Activity Layout.

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.mysecondapp.ThreadsActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">

        <ImageView
            android:id="@+id/imageView_picture"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:background="@android:color/black" />

        <Button
            android:id="@+id/button_get_picture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@id/imageView_picture"
            android:onClick="getPicture"
            android:text="Get Picture" />

        <Button
            android:id="@+id/button_clear_picture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@id/button_get_picture"
            android:layout_toEndOf="@id/button_get_picture"
            android:layout_toRightOf="@id/button_get_picture"
            android:onClick="clearPicture"
            android:text="Clear Picture" />

        <ProgressBar
            android:id="@+id/progressBar_loading"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/button_get_picture"
            android:progress="0"
            android:indeterminateOnly="false"
            android:visibility="invisible" />

    </RelativeLayout>
</ScrollView>

The Activity with: subclassed AsyncTask inner class; subclassed RetainedFragment inner class that handles runtime configuration changes (e.g. when the user rotates the screen); and a determinate progress bar updating at regular intervals. ...

public class ThreadsActivity extends Activity {

    private ImageView mPictureImageView;
    private RetainedFragment mRetainedFragment = null;
    private ProgressBar mLoadingProgressBar;

    public static class RetainedFragment extends Fragment {

        private Bitmap mBitmap;

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

            // The key to making data survive runtime configuration changes.
            setRetainInstance(true);
        }

        public Bitmap getData() {
            return this.mBitmap;
        }

        public void setData(Bitmap bitmapToRetain) {
            this.mBitmap = bitmapToRetain;
        }
    }

    private class LoadImageFromNetworkAsyncTask extends AsyncTask<String,
            Integer, Bitmap> {

        @Override
        protected Bitmap doInBackground(String... urls) {
            // Simulate a burdensome load.
            int sleepSeconds = 4;
            for (int i = 1; i <= sleepSeconds; i++) {
                SystemClock.sleep(1000); // milliseconds
                publishProgress(i * 20); // Adjust for a scale to 100
            }

            return com.example.standardapplibrary.android.Network
                    .loadImageFromNetwork(
                    urls[0]);
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            mLoadingProgressBar.setProgress(progress[0]);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            publishProgress(100);
            mRetainedFragment.setData(bitmap);
            mPictureImageView.setImageBitmap(bitmap);
            mLoadingProgressBar.setVisibility(View.INVISIBLE);
            publishProgress(0);
        }
    }

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

        final String retainedFragmentTag = "RetainedFragmentTag";

        mPictureImageView = (ImageView) findViewById(R.id.imageView_picture);
        mLoadingProgressBar = (ProgressBar) findViewById(R.id.progressBar_loading);

        // Find the RetainedFragment on Activity restarts
        FragmentManager fm = getFragmentManager();
        // The RetainedFragment has no UI so we must reference it with a tag.
        mRetainedFragment = (RetainedFragment) fm.findFragmentByTag(
                retainedFragmentTag);

        // if Retained Fragment doesn't exist create and add it.
        if (mRetainedFragment == null) {

            // Add the fragment
            mRetainedFragment = new RetainedFragment();
            fm.beginTransaction().add(mRetainedFragment,
                                      retainedFragmentTag).commit();

            // The Retained Fragment exists
        } else {

            mPictureImageView.setImageBitmap(mRetainedFragment.getData());
        }
    }

    public void getPicture(View view) {
        mLoadingProgressBar.setVisibility(View.VISIBLE);
        new LoadImageFromNetworkAsyncTask().execute(
                "http://i.imgur.com/SikTbWe.jpg");
    }

    public void clearPicture(View view) {
        mRetainedFragment.setData(null);
        mPictureImageView.setImageBitmap(null);
    }
}

In this example the library function (referenced above with the explicit package prefix com.example.standardapplibrary.android.Network) that does real work ...

public static Bitmap loadImageFromNetwork(String url) {
    Bitmap bitmap = null;
    try {
        bitmap = BitmapFactory.decodeStream((InputStream) new URL(url)
                .getContent());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return bitmap;
}

Add any permissions that your background task requires to the AndroidManifest.xml ...

<manifest>
...
    <uses-permission android:name="android.permission.INTERNET" />

Add your activity to AndroidManifest.xml ...

<manifest>
...
    <application>
        <activity
            android:name=".ThreadsActivity"
            android:label="@string/title_activity_threads"
            android:parentActivityName=".MainActivity">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.example.mysecondapp.MainActivity" />
        </activity>
Polypoid answered 11/8, 2014 at 10:47 Comment(6)
Great. You should write a blog on this.Tallent
@AKh. Do you mean to suggest my answer takes up too much room on Stackoverflow?Polypoid
I think he just means you have an awesome answer & you should write a blog! =) @JohnBentleyIsolated
@SandyD.yesterday Thanks for the positive interpretation. I do hope she or he intended that.Polypoid
I also thought it was an awesome answer and I interpreted it like that as well. Very complete answers like this are great!!Lasala
Thanks for the feedback @LeonardoSibela. I'm glad it was of value to you.Polypoid
X
3

Recently, I've found a good solution here. It is based on the saving a task object via on RetainConfiguration. To my point of view, the solution is very elegant and as for me I've started to use it. You need just to nest your asynctask from the basetask and that's all.

Xyloid answered 5/7, 2012 at 8:12 Comment(2)
Thank you very much for this interesting answer. It's a good solution in addition to the ones mentioned in the related question.Islington
Unfortunately, this solution uses deprecated methods.Ellersick
P
3

Based on @Alex Lockwood answer and on @William & @quickdraw mcgraw answers on this post : How to handle Handler messages when activity/fragment is paused, I wrote a generic solution.

This way the rotation is handled, and if the activity goes to background during the async task execution, the activity will receive the callbacks (onPreExecute, onProgressUpdate, onPostExecute & onCancelled) once resumed, so no IllegalStateException will be thrown (see How to handle Handler messages when activity/fragment is paused).

It would be great to have the same but with generic arguments types, like an AsyncTask (eg: AsyncTaskFragment<Params, Progress, Result>), but I did not manage to do it quickly and have no time at the moment. If anyone want to do the improvement, please feel free !

The code:

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;

public class AsyncTaskFragment extends Fragment {

    /* ------------------------------------------------------------------------------------------ */
    // region Classes & Interfaces

    public static abstract class Task extends AsyncTask<Object, Object, Object> {

        private AsyncTaskFragment _fragment;

        private void setFragment(AsyncTaskFragment fragment) {

            _fragment = fragment;
        }

        @Override
        protected final void onPreExecute() {

            // Save the state :
            _fragment.setRunning(true);

            // Send a message :
            sendMessage(ON_PRE_EXECUTE_MESSAGE, null);
        }

        @Override
        protected final void onPostExecute(Object result) {

            // Save the state :
            _fragment.setRunning(false);

            // Send a message :
            sendMessage(ON_POST_EXECUTE_MESSAGE, result);
        }

        @Override
        protected final void onProgressUpdate(Object... values) {

            // Send a message :
            sendMessage(ON_PROGRESS_UPDATE_MESSAGE, values);
        }

        @Override
        protected final void onCancelled() {

            // Save the state :
            _fragment.setRunning(false);

            // Send a message :
            sendMessage(ON_CANCELLED_MESSAGE, null);
        }

        private void sendMessage(int what, Object obj) {

            Message message = new Message();
            message.what = what;
            message.obj = obj;

            Bundle data = new Bundle(1);
            data.putString(EXTRA_FRAGMENT_TAG, _fragment.getTag());
            message.setData(data);

            _fragment.handler.sendMessage(message);
        }
    }

    public interface AsyncTaskFragmentListener {

        void onPreExecute(String fragmentTag);
        void onProgressUpdate(String fragmentTag, Object... progress);
        void onCancelled(String fragmentTag);
        void onPostExecute(String fragmentTag, Object result);
    }

    private static class AsyncTaskFragmentPauseHandler extends PauseHandler {

        @Override
        final protected void processMessage(Activity activity, Message message) {

            switch (message.what) {

                case ON_PRE_EXECUTE_MESSAGE : { ((AsyncTaskFragmentListener)activity).onPreExecute(message.getData().getString(EXTRA_FRAGMENT_TAG)); break; }
                case ON_POST_EXECUTE_MESSAGE : { ((AsyncTaskFragmentListener)activity).onPostExecute(message.getData().getString(EXTRA_FRAGMENT_TAG), message.obj); break; }
                case ON_PROGRESS_UPDATE_MESSAGE : { ((AsyncTaskFragmentListener)activity).onProgressUpdate(message.getData().getString(EXTRA_FRAGMENT_TAG), ((Object[])message.obj)); break; }
                case ON_CANCELLED_MESSAGE : { ((AsyncTaskFragmentListener)activity).onCancelled(message.getData().getString(EXTRA_FRAGMENT_TAG)); break; }
            }
        }
    }

    // endregion
    /* ------------------------------------------------------------------------------------------ */



    /* ------------------------------------------------------------------------------------------ */
    // region Attributes

    private Task _task;
    private AsyncTaskFragmentListener _listener;
    private boolean _running = false;

    private static final String EXTRA_FRAGMENT_TAG = "EXTRA_FRAGMENT_TAG";
    private static final int ON_PRE_EXECUTE_MESSAGE = 0;
    private static final int ON_POST_EXECUTE_MESSAGE = 1;
    private static final int ON_PROGRESS_UPDATE_MESSAGE = 2;
    private static final int ON_CANCELLED_MESSAGE = 3;

    private AsyncTaskFragmentPauseHandler handler = new AsyncTaskFragmentPauseHandler();

    // endregion
    /* ------------------------------------------------------------------------------------------ */



    /* ------------------------------------------------------------------------------------------ */
    // region Getters

    public AsyncTaskFragmentListener getListener() { return _listener; }
    public boolean isRunning() { return _running; }

    // endregion
    /* ------------------------------------------------------------------------------------------ */



    /* ------------------------------------------------------------------------------------------ */
    // region Setters

    public void setTask(Task task) {

        _task = task;
        _task.setFragment(this);
    }

    public void setListener(AsyncTaskFragmentListener listener) { _listener = listener; }
    private void setRunning(boolean running) { _running = running; }

    // endregion
    /* ------------------------------------------------------------------------------------------ */



    /* ------------------------------------------------------------------------------------------ */
    // region Fragment lifecycle

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    @Override
    public void onResume() {

        super.onResume();
        handler.resume(getActivity());
    }

    @Override
    public void onPause() {

        super.onPause();
        handler.pause();
    }

    @Override
    public void onAttach(Activity activity) {

        super.onAttach(activity);
        _listener = (AsyncTaskFragmentListener) activity;
    }

    @Override
    public void onDetach() {

        super.onDetach();
        _listener = null;
    }

    // endregion
    /* ------------------------------------------------------------------------------------------ */



    /* ------------------------------------------------------------------------------------------ */
    // region Utils

    public void execute(Object... params) {

        _task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
    }

    public void cancel(boolean mayInterruptIfRunning) {

        _task.cancel(mayInterruptIfRunning);
    }

    public static AsyncTaskFragment getRetainedOrNewFragment(AppCompatActivity activity, String fragmentTag) {

        FragmentManager fm = activity.getSupportFragmentManager();
        AsyncTaskFragment fragment = (AsyncTaskFragment) fm.findFragmentByTag(fragmentTag);

        if (fragment == null) {

            fragment = new AsyncTaskFragment();
            fragment.setListener( (AsyncTaskFragmentListener) activity);
            fm.beginTransaction().add(fragment, fragmentTag).commit();
        }

        return fragment;
    }

    // endregion
    /* ------------------------------------------------------------------------------------------ */
}

You'll need the PauseHandler :

import android.app.Activity;
import android.os.Handler;
import android.os.Message;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Message Handler class that supports buffering up of messages when the activity is paused i.e. in the background.
 *
 * https://mcmap.net/q/159989/-how-to-handle-handler-messages-when-activity-fragment-is-paused
 */
public abstract class PauseHandler extends Handler {

    /**
     * Message Queue Buffer
     */
    private final List<Message> messageQueueBuffer = Collections.synchronizedList(new ArrayList<Message>());

    /**
     * Flag indicating the pause state
     */
    private Activity activity;

    /**
     * Resume the handler.
     */
    public final synchronized void resume(Activity activity) {
        this.activity = activity;

        while (messageQueueBuffer.size() > 0) {
            final Message msg = messageQueueBuffer.get(0);
            messageQueueBuffer.remove(0);
            sendMessage(msg);
        }
    }

    /**
     * Pause the handler.
     */
    public final synchronized void pause() {
        activity = null;
    }

    /**
     * Store the message if we have been paused, otherwise handle it now.
     *
     * @param msg   Message to handle.
     */
    @Override
    public final synchronized void handleMessage(Message msg) {
        if (activity == null) {
            final Message msgCopy = new Message();
            msgCopy.copyFrom(msg);
            messageQueueBuffer.add(msgCopy);
        } else {
            processMessage(activity, msg);
        }
    }

    /**
     * Notification message to be processed. This will either be directly from
     * handleMessage or played back from a saved message when the activity was
     * paused.
     *
     * @param activity  Activity owning this Handler that isn't currently paused.
     * @param message   Message to be handled
     */
    protected abstract void processMessage(Activity activity, Message message);
}

Sample usage:

public class TestActivity extends AppCompatActivity implements AsyncTaskFragmentListener {

    private final static String ASYNC_TASK_FRAGMENT_A = "ASYNC_TASK_FRAGMENT_A";
    private final static String ASYNC_TASK_FRAGMENT_B = "ASYNC_TASK_FRAGMENT_B";

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        Button testButton = (Button) findViewById(R.id.test_button);
        final AsyncTaskFragment fragment = AsyncTaskFragment.getRetainedOrNewFragment(TestActivity.this, ASYNC_TASK_FRAGMENT_A);

        testButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if(!fragment.isRunning()) {

                    fragment.setTask(new Task() {

                        @Override
                        protected Object doInBackground(Object... objects) {

                            // Do your async stuff

                            return null;
                        }
                    });

                    fragment.execute();
                }
            }
        });
    }

    @Override
    public void onPreExecute(String fragmentTag) {}

    @Override
    public void onProgressUpdate(String fragmentTag, Float percent) {}

    @Override
    public void onCancelled(String fragmentTag) {}

    @Override
    public void onPostExecute(String fragmentTag, Object result) {

        switch (fragmentTag) {

            case ASYNC_TASK_FRAGMENT_A: {

                // Handle ASYNC_TASK_FRAGMENT_A
                break;
            }
            case ASYNC_TASK_FRAGMENT_B: {

                // Handle ASYNC_TASK_FRAGMENT_B
                break;
            }
        }
    }
}
Parcheesi answered 16/7, 2015 at 13:29 Comment(0)
A
3

You can use Loaders for this. Check Doc here

Atkinson answered 13/9, 2017 at 19:30 Comment(2)
Loaders are now deprecated as of Android API 28 (as the link will tell you).Gristly
Loaders aren't deprecated, it's only how you call them that's changedWollis
A
2

For those who wants to dodge Fragments, you can retain the AsyncTask running on orientation changes using onRetainCustomNonConfigurationInstance() and some wiring.

(Notice that this method is the alternative to the deprecated onRetainNonConfigurationInstance()).

Seems like this solution is not frequently mentioned though. I wrote a simple running example to illustrate.

Cheers!

public class MainActivity extends AppCompatActivity {

private TextView result;
private Button run;
private AsyncTaskHolder asyncTaskHolder;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    result = (TextView) findViewById(R.id.textView_result);
    run = (Button) findViewById(R.id.button_run);
    asyncTaskHolder = getAsyncTaskHolder();
    run.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            asyncTaskHolder.execute();
        }
    });
}

private AsyncTaskHolder getAsyncTaskHolder() {
    if (this.asyncTaskHolder != null) {
        return asyncTaskHolder;
    }
    //Not deprecated. Get the same instance back.
    Object instance = getLastCustomNonConfigurationInstance();

    if (instance == null) {
        instance = new AsyncTaskHolder();
    }
    if (!(instance instanceof ActivityDependant)) {
        Log.e("", instance.getClass().getName() + " must implement ActivityDependant");
    }
    return (AsyncTaskHolder) instance;
}

@Override
//Not deprecated. Save the object containing the running task.
public Object onRetainCustomNonConfigurationInstance() {
    return asyncTaskHolder;
}

@Override
protected void onStart() {
    super.onStart();
    if (asyncTaskHolder != null) {
        asyncTaskHolder.attach(this);
    }
}

@Override
protected void onStop() {
    super.onStop();
    if (asyncTaskHolder != null) {
        asyncTaskHolder.detach();
    }
}

void updateUI(String value) {
    this.result.setText(value);
}

interface ActivityDependant {

    void attach(Activity activity);

    void detach();
}

class AsyncTaskHolder implements ActivityDependant {

    private Activity parentActivity;
    private boolean isRunning;
    private boolean isUpdateOnAttach;

    @Override
    public synchronized void attach(Activity activity) {
        this.parentActivity = activity;
        if (isUpdateOnAttach) {
            ((MainActivity) parentActivity).updateUI("done");
            isUpdateOnAttach = false;
        }
    }

    @Override
    public synchronized void detach() {
        this.parentActivity = null;
    }

    public synchronized void execute() {
        if (isRunning) {
            Toast.makeText(parentActivity, "Already running", Toast.LENGTH_SHORT).show();
            return;
        }
        isRunning = true;
        new AsyncTask<Void, Integer, Void>() {

            @Override
            protected Void doInBackground(Void... params) {
                for (int i = 0; i < 100; i += 10) {
                    try {
                        Thread.sleep(500);
                        publishProgress(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return null;
            }

            @Override
            protected void onProgressUpdate(Integer... values) {
                if (parentActivity != null) {
                    ((MainActivity) parentActivity).updateUI(String.valueOf(values[0]));
                }
            }

            @Override
            protected synchronized void onPostExecute(Void aVoid) {
                if (parentActivity != null) {
                    ((MainActivity) parentActivity).updateUI("done");
                } else {
                    isUpdateOnAttach = true;
                }
                isRunning = false;
            }
        }.execute();
    }
}
Amphigory answered 24/5, 2017 at 22:52 Comment(0)
C
0

I have implemented library that can solve problems with activity pause and recreation while your task is executing.

You should implement AsmykPleaseWaitTask and AsmykBasicPleaseWaitActivity. Your activity and background task will work fine even you are will rotate screen and switch between applications

Celisse answered 31/5, 2017 at 15:56 Comment(0)
M
-9

FAST WORKAROUND (not recomended)

To avoid an Activity to destroy and create itself is to declare your activity in manifest file: android:configChanges="orientation|keyboardHidden|screenSize

  <activity
        android:name=".ui.activity.MyActivity"
        android:configChanges="orientation|keyboardHidden|screenSize"
        android:label="@string/app_name">

As it's mentioned in docs

The screen orientation has changed — the user has rotated the device.

Note: If your application targets API level 13 or higher (as declared by the minSdkVersion and targetSdkVersion attributes), then you should also declare the "screenSize" configuration, because it also changes when a device switches between portrait and landscape orientations.

Mantilla answered 9/11, 2015 at 14:7 Comment(1)
This is best avoided. developer.android.com/guide/topics/resources/… "Note: Handling the configuration change yourself can make it much more difficult to use alternative resources, because the system does not automatically apply them for you. This technique should be considered a last resort when you must avoid restarts due to a configuration change and is not recommended for most applications."Powder

© 2022 - 2024 — McMap. All rights reserved.