Stop using AsyncTasks for networking operations right now. Like, Right Now.

… or at least CANCEL it

You can do it of course. You can use AsyncTask for networking operations, but don’t. AsyncTasks should ideally be used for short operations (a few seconds at the most). Are you sure that your networking operation can be done in few seconds? I’m almost never sure.

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 package such as
Executor, ThreadPoolExecutor and FutureTask.

― Official Android Developer documentation.

Yeah, but AsyncTasks are so easy to use

That is true. AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.

Using AsyncTask we want to execute a task in the background and deliver a result back to the UI thread.

Minimal implementation of AsyncTask

public class MinimalAsyncTask extends AsyncTask<Params, Void, Result> {
    
    @Override
    protected Result doInBackground(Params... param) {
        //do some task on background thread
    }

}

The only method you need to override in the class is doInBackground().

doInBackground() is the only abstract method defined in AsyncTask class so you have to override it. Also, that’s the only method that runs on a background thread.

But if you implement AsyncTask without the callback that give results back to the UI (Main) thread (onPostExecute(Result result)), the AsyncTask is only a background task.

You can make one more problem and increase complexity of AsyncTask – implementing it without parameters:

AsyncTask<Void, Void, Void>

If you don’t define any parameters than AsyncTask can’t pass data between the UI thread and the background thread. You can’t enter data into the background thread, can’t report progress, and can’t pass back result from the background thread to the UI thread.

The AsyncTask is executed by calling the execute method, which triggers a callback to doInBackground on a background thread:

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        new MinimalAsyncTask().execute(Params... param);
        ...
    }

}

Params are Objects of any type that can be passed from the initiating thread (Main or UI thread) to the background thread. Usually params are URLs (URLs for getting or posting data from / to Server).

More “normal” usage of AsyncTask

public class DataFetcherAsyncTask extends AsyncTask<Params, Progress, Result> {
    
    @Override
    protected void onPreExecute() { ... }
    
    @Override
    protected Result doInBackground(Params... param) {
        try {
            if (!isCancelled()) {
                doSomeLongOperation(param[0]);
            }
        } catch (Exception e) {
            // Do nothing. Or just print error.
        }
	
        return Result;
    }

    @Override
    protected void onProgressUpdate(Progress... progress) { ... }

    @Override
    protected void onPostExecute(Result result) { ... }

    @Override
    protected void onCancelled(Result result) { ... }

}

If UI thread decides not to use the results of an AsyncTask, it can send a termination request through a call to cancel(boolean). That can happen if user put application to the background or leave the Activity or Fragment.

public class MainActivity extends AppCompatActivity {
    
    private DataFetcherAsyncTask mDataFetcherAsyncTask;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mDataFetcherAsyncTask = new DataFetcherAsyncTask().execute(Params... param);
        ...
    }

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

        // Cancel the task. Interrupt background thread.
        mDataFetcherAsyncTask.cancel(true);
    }

    ...

}

Reasons for proper cancellation

Well, imagine you open activity and start AsyncTask in onCreate method. Rotate the phone screen once and after that rotate the screen 20 or maybe 30 times without pause!

Every screen rotation will call onDestroy in previous activity and recreate new activity from the beginning (onCreate). This way Android loads different layouts for Portrait and Landscape modes.

Screen rotation will kill Activity but will NOT kill AsyncTask. So, if you rotate the screen 20 times you will have 20 AsyncTasks running for the same data from the server (doing the same job).

Even worse, all 20 AsycTasks, after onPostExecute, will try to update UI elements that are not there anymore (destroyed by screen rotation and recreated again as new instances). This can cause Memory Leak problem if you declare UI elements as static (keeping objects in memory and not destroying them).

The AsyncTask should therefore be declared as a standalone or static inner class so that the worker thread avoids implicit references to outer classes. Using non-static inner classes for long running operations is always a bad practice, not just in Android.

One way of proper cancellation

When it receives a cancellation request, the task skips the call to onPostExecute and calls one of the cancel callbacks — onCancelled() or onCancelled(Result) — instead. You can use the cancel callbacks to update the UI with a different result or a different message to the user notifying him about task cancellation.

An AsyncTask has the following possible states: PENDING, RUNNING and FINISHED, in that order:

  • PENDING, The AsyncTask instance is created, but execute has not been called on it.
  • RUNNING, Execution has started; i.e., execute is called. The task remains in this state when it finishes, as long as its final method (such as onPostExecute) is still running.
  • FINISHED, Both the background execution and the optional final operation—onPostExecute or onCancelled—is done.
public class StandaloneActivity extends AppCompatActivity {
    
    private static final String IMAGE_TO_DOWNLOAD_URL = "http://...";

    private DataFetcherAsyncTask mDataFetcherAsyncTask;
    public ProgressBar mProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_file_download);
        
        mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
        ...
        fetchData();
        ...
    }

    private void fetchData() {
        mDataFetcherAsyncTask = new DataFetcherAsyncTask(this);
        mDataFetcherAsyncTask.execute(IMAGE_TO_DOWNLOAD_URL);
    }

    public void onDataFetched(Bitmap bitmap) {
        
    }

    public void onDataCancelled() {
        
    }

    private void reloadData() {
        if (mDataFetcherAsyncTask != null && mDataFetcherAsyncTask.getStatus() != AsyncTask.Status.RUNNING) {
            fetchData();
        }
    }

    @Override
    protected void onDestroy() {
        // Cancel the task. Interrupt background thread

        mDataFetcherAsyncTask.cancel(true);
    }

    ...

}
public class DataFetcherAsyncTask extends AsyncTask<String, Void, Bitmap> {
    
    private ImageDownloadActivity mActivity;

    public DataFetcherAsyncTask(ImageDownloadActivity activity) {
        mActivity = activity;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        if (mActivity != null) {
            mActivity.mProgressBar.setVisibility(View.VISIBLE);
        }
    }
    
    @Override
    protected Bitmap doInBackground(String... param) {
        Bitmap bitmap = null;
        try {
            if (!isCancelled()) {
                bitmap = downloadImageFile(param[0]);
            }
        } catch (Exception e) {
            // Do nothing. Or just print error.
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        if (mActivity != null) {
            mActivity.mProgressBar.setVisibility(View.GONE);
            mActivity.onDataFetched(bitmap);
        }
    }

    @Override
    protected void onCancelled() {
        super.onCancelled();
        if (mActivity != null) {
            mActivity.mProgressBar.setVisibility(View.GONE);
            mActivity.onDataCancelled();
        }
    }

    private Bitmap downloadImageFile(String url) {
        Bitmap bitmap = null;
        try {
            bitmap = BitmapFactory.decodeStream((InputStream) new URL(url).getContent());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

}

Static inner class and WeakReference

Personally I like it more when Activities and AsyncTasks are separated into different files. This way your code is more readable and you can reuse it on different places in the application. But you can also define AsyncTask in Activity as a static inner class and use WeakReference. Defining AsyncTask as static inner class the worker thread avoids implicit references to outer classes. This way you avoiding memory leaks.

Most memory leaks involving threads are caused by objects staying in memory longer than required. Threads and handlers can keep objects unreachable from a thread GC root even when they are not used anymore.

Inner classes hold implicit references to the outer class they are declared in. So, they can leak not only their own objects, but also those referenced from the outer class. It is preferred to use static inner classes because they reference only the global class object and not the instance object. All explicit references to other instance objects from the static inner class are still live while the thread executes.

Static inner classes do not have access to instance fields of the outer class. This can be a limitation if an application would like to execute a task on a worker thread and access or update an instance field of the outer instance. For this need, java.lang.ref.WeakReference can help.

After each configuration change, the Android system creates a new Activity and leaves the old one behind to be garbage collected. However, if the thread holds an implicit reference to the old Activity and prevents it from ever being reclaimed. As a result, each new Activity is leaked and all resources associated with them are never able to be reclaimed.

The fix is easy: declare the AsyncTask as a private static inner class as shown below.

public class StaticInnerClassActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        new DataFetcherAsyncTask(new WeakReference<>(this));
        ...
    }

    ...

    /**
     * Static inner classes don't hold implicit references to their
     * enclosing class, so the Activity instance won't be leaked across
     * configuration changes.
     */
    private static class DataFetcherAsyncTask extends AsyncTask<String, Void, Bitmap> {
    
        private WeakReference<StaticInnerClassActivity> mWeakActivity;

        public DataFetcherAsyncTask(WeakReference<StaticInnerClassActivity> activity) {
            this.mWeakActivity = activity;
        }

        @Override
        protected Bitmap doInBackground(String... param) {
           Bitmap bitmap = null;
            try {
                if (!isCancelled()) {
                    bitmap = downloadImageFile(param[0]);
                }
            } catch (Exception e) {
                // Do nothing. Or just print error.
            }
            return bitmap;
        }

        ...

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            
            ImageDownloadActivity localReferenceActivity = mWeakActivity.get();
            if (localReferenceActivity != null) {
                localReferenceActivity.mProgressBar.setVisibility(View.GONE);
                localReferenceActivity.onDataFetched(bitmap);
            }
        }
    }

}

Reuse of the AsyncTask code – Interfaces to the rescue

So, if you want to use AsyncTask on more than one place in the app, and don’t want to duplicate your code, the best way is by implementing interfaces.

Imagine you have one app page that have to download image from the server and display it. And you have also another page that have to download different image from the server and display it. Why not to reuse the code and let the same AsyncTask finish the job.

public interface IDownloadImageAsyncTaskHolder {

    void onDataFetched(Bitmap bitmap);
    void onDataCancelled();
    void showProgressBar();
    void hideProgressBar(); 

}
public class InterfaceOneActivity extends AppCompatActivity
                                implements IDownloadImageAsyncTaskHolder {

    private DownloadImageFileAsyncTask mDataFetcherAsyncTask;

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

        setContentView(R.layout.activity_interface_one);
        ...
        fetchData();
        ...
    }

    private void fetchData() {
        mDataFetcherAsyncTask = new DownloadImageFileAsyncTask(this);
        mDataFetcherAsyncTask.execute(imageOneUrl);
    }

    @Override
    public void onDataFetched(Bitmap bitmap) {
        if (bitmap != null) mImageView.setImageBitmap(bitmap);
    }

    @Override
    public void onDataCancelled() {
        if (mProgressBar != null) mProgressBar.setVisibility(View.GONE);
    }

    @Override
    public void showProgressBar() {
        mProgressBar.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideProgressBar() {
        mProgressBar.setVisibility(View.GONE);
    }

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

        mDataFetcherAsyncTask.cancel(true);
    }

    ...
}
public class InterfaceTwoActivity extends AppCompatActivity 
                                implements IDownloadImageAsyncTaskHolder {

    private DownloadImageFileAsyncTask mDataFetcherAsyncTask;

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

        setContentView(R.layout.activity_interface_two);
        ...
        fetchData();
        ...
    }

    private void fetchData() {
        mDataFetcherAsyncTask = new DownloadImageFileAsyncTask(this);
        mDataFetcherAsyncTask.execute(imageTwoUrl);
    }

    @Override
    public void onDataFetched(Bitmap bitmap) {
        if (bitmap != null) mImageView.setImageBitmap(bitmap);
    }

    @Override
    public void onDataCancelled() {
        if (mProgressBar != null) mProgressBar.setVisibility(View.GONE);
    }

    @Override
    public void showProgressBar() {
        mProgressBar.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideProgressBar() {
        mProgressBar.setVisibility(View.GONE);
    }

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

        mDataFetcherAsyncTask.cancel(true);
    }

    ...
}
public class DownloadImageFileAsyncTask extends AsyncTask<String, Void, Bitmap> {

    private IDownloadImageAsyncTaskHolder mActivity;

    public DownloadImageFileAsyncTask(IDownloadImageAsyncTaskHolder activity) {
        mActivity = activity;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        if (mActivity != null) {
            mActivity.showProgressBar();
        }
    }

    @Override
    protected Bitmap doInBackground(String... param) {
        Bitmap bitmap = null;
        try {
            if (!isCancelled()) {
                bitmap = downloadImageFile(param[0]);
            }
        } catch (Exception e) {
            // Do nothing. Or just print error.
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        if (mActivity != null) {
            mActivity.hideProgressBar();
            mActivity.onDataFetched(bitmap);
        }
    }

    @Override
    protected void onCancelled() {
        super.onCancelled();
        if (mActivity != null) {
            mActivity.hideProgressBar();
            mActivity.onDataCancelled();
        }
    }

}

Bad practice – Not Cancelled AsyncTask

public class NotCancelledAsyncTask extends AsyncTask<String, Void, Bitmap> {

    private NotCancelledActivity mActivity;

    public NotCancelledAsyncTask(NotCancelledActivity activity) {
        mActivity = activity;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.mProgressBar.setVisibility(View.VISIBLE);
    }

    @Override
    protected Bitmap doInBackground(String... param) {
        return downloadImageFile(param[0]);
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        mActivity.mProgressBar.setVisibility(View.GONE);
        mActivity.onDataFetched(bitmap);
    }

    private Bitmap downloadImageFile(String url) {
        Bitmap bitmap = null;
        try {
            bitmap = BitmapFactory.decodeStream((InputStream) new URL(url).getContent());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

Android Monitor and Cancelled vs Not Cancelled AsyncTask

Cancelled AsyncTask is displayed on the left and not cancelled AsyncTask is displayed on the right. This is what we have if we rotate screen non stop.

Cancelled Not Cancelled AsyncTask

It’s obvious on the first look that not cancelled AsyncTask can produce memory leak. Also, if AsyncTask is not cancelled, memory usage is huge.

Bad practice – Not Static Inner Class AsyncTask

public class NotStaticInnerClassActivity extends AppCompatActivity {

    private static final String IMAGE_TO_DOWNLOAD_URL = "...";

    private DataFetcherAsyncTask mDataFetcherAsyncTask;
    public ProgressBar mProgressBar;
    private ImageView mImageView;

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

        ...
        fetchData();
        ...
    }

    private void fetchData() {
        mImageView.setImageBitmap(null);

        mDataFetcherAsyncTask = new DataFetcherAsyncTask(this);
        mDataFetcherAsyncTask.execute(IMAGE_TO_DOWNLOAD_URL);
    }

    ...

    /**
     * Non Static inner classes will hold implicit references to their
     * enclosing class, so the Activity instance will be leaked across
     * configuration changes.
     */
    private class DataFetcherAsyncTask extends AsyncTask<String, Void, Bitmap> {

        private NotStaticInnerClassActivity mActivity;

        public DataFetcherAsyncTask(NotStaticInnerClassActivity activity) {
            this.mActivity = activity;
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            mActivity.mProgressBar.setVisibility(View.VISIBLE);
        }

        @Override
        protected Bitmap doInBackground(String... param) {
            return downloadImageFile(param[0]);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);

            mActivity.mProgressBar.setVisibility(View.GONE);
            mActivity.onDataFetched(bitmap);
        }

        private Bitmap downloadImageFile(String url) {
            Bitmap bitmap = null;
            try {
                bitmap = BitmapFactory.decodeStream((InputStream) new URL(url).getContent());
            } catch (IOException e) {
                e.printStackTrace();
            }
            return bitmap;
        }
    }

    ...

}

Android Monitor and Static vs Not Static Inner Class AsyncTask

Static AsyncTask is displayed on the left and not static AsyncTask is displayed on the right. This is what we have if we rotate screen non stop.

Static Not Static inner AsyncTask

It’s obvious on the first look that not static inner class AsyncTask can produce memory leak. Also, if not static inner class AsyncTask is not cancelled, memory usage is huge.

And now the real fun begins. TESTING!

So, in Android we have JUnit test that runs on the local JVM or an instrumented test that runs on a device. You can also extend your test capabilities by integrating test frameworks such as Mockito to test Android API calls in your local unit tests, and Espresso or UI Automator to exercise user interaction in your instrumented tests.

Instrumented tests are tests that run on a hardware device or emulator. These tests have access to Instrumentation APIs, give you access to information such as the Context of the app you are testing, and let you control the app under test from your test code.

Use these tests when writing integration and functional UI tests to automate user interaction, or when your tests have Android dependencies that mock objects cannot satisfy.

In progress. More will follow soon…

Leave a Reply