Handle Android AsyncTask Configuration Change Using Fragment
AsyncTask is much preferred way for background processing of some task with easy use of UI thread. It powers you to peform background work and publish results to UI thread without manipulating Threads or Handlers. General usage of AsyncTask includes performing short network or intensive data operations.
We can use IntentService in conjunction with ResultReceiver also for achieving same result. If interested, you can check our detailed tutorial. Android IntentService: Working and Advantages
With these bundled benefits of AsyncTask, it provides hard time to handle it over configuration change. During configuration change if not handled, it may lead to following problems.
1. Memory Leak : Problem with AsyncTask is that it has an implicit reference of enclosing activity. In case of configuration change if not handled properly, enclosing activity instance will be destroyed but it will not be garbage collected until AsyncTask is running. In case of multiple async tasks running, it may lead to memory leakage. While using AsyncTask you should try that enclosing destroyed activity is freed from memory.
2. Lost Progress and Result: If AsyncTask is used with intention to publish progress and results to Activity UI, they will be discarded as previous activity instance is destroyed. It’s good practice to utilize previous results and progress to current enclosing instance of activity UI update rather than performing same task again.
We will encounter above two problem using Headless Fragments. Fragment which do not have UI are referred as Headless Fragment as they are not part of user interaction. Headless fragments are meant for data encapsulation and can be used for data processing sharing. We will make most out of fragment’s retaining property which can be set using Fragment.setRetainInstance(boolean). If true, fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack.
You can check out our tutorial on Fragments for their detailed benefits and usage.
Android Fragments: Building dynamic UI, Why to use Fragments?
Sample Android Application
1. Create new android application project with package name “com.androidsrc.headlessfragment” and application name as “HeadlessFragment”.
2. Preparing Application Manifest File
We will have only one activity in our application for Handling Android AsyncTask Configuration Change. No permission is required to mention. Final AndroidManifest.xml will be as below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.androidsrc.headlessfragment" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> |
3. Preparing Layout File
Our main activity layout /res/layout/activity_main.xml will have ProgressBar and TextView to show progress of AsyncTask running in Fragment. We will have two Button views for starting and cancelling task and one Button view for recreating current activity as in case of configuration change.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
<LinearLayout 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" android:orientation="vertical" android:padding="20dp" tools:context="com.androidsrc.headlessfragment.MainActivity" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="left" android:text="Progress" /> <TextView android:id="@+id/progressValue" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="right" android:text="0%" /> </LinearLayout> <ProgressBar style="?android:attr/progressBarStyleHorizontal" android:id="@+id/progressBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" /> <LinearLayout style="?android:attr/buttonBarButtonStyle" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:orientation="horizontal" > <Button android:id="@+id/start" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:onClick="onClick" android:text="Start Task" /> <Button android:id="@+id/cancel" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:onClick="onClick" android:text="Cancel Task" /> </LinearLayout> <Button android:id="@+id/recreate" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:gravity="center" android:text="Tap to recreate activity" /> </LinearLayout> |
4. Create a new class HeadlessFragment which will extend Fragment class. Define an interface TaskStatusCallback inside this class which will be used as callback for notifying AsyncTask status back to Activity.
1 2 3 4 5 6 7 8 9 |
public static interface TaskStatusCallback { void onPreExecute(); void onProgressUpdate(int progress); void onPostExecute(); void onCancelled(); } |
Override onAttach(), onCreate() and onDetach() to control current active instance for your callback. In onCreate() make sure to make fragment retainable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
TaskStatusCallback mStatusCallback; BackgroundTask mBackgroundTask; boolean isTaskExecuting = false; /** * Called when a fragment is first attached to its activity. * onCreate(Bundle) will be called after this. */ @Override public void onAttach(Activity activity) { super.onAttach(activity); mStatusCallback = (TaskStatusCallback)activity; } /** * Called to do initial creation of a fragment. * This is called after onAttach(Activity) and before onCreateView(LayoutInflater, ViewGroup, Bundle) */ @Override public void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setRetainInstance(true); } /** * Called when the fragment is no longer attached to its activity. This is called after onDestroy(). */ @Override public void onDetach() { // TODO Auto-generated method stub super.onDetach(); mStatusCallback = null; } |
Define a private local class BackgroundTask which will extend to AsyncTak and will run the task request from activity in background. We will use callback to update it’s status or progress back to Activity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
private class BackgroundTask extends AsyncTask<Void, Integer, Void> { @Override protected void onPreExecute() { if(mStatusCallback != null) mStatusCallback.onPreExecute(); } @Override protected Void doInBackground(Void... params) { int progress = 0; while(progress < 100 && !isCancelled()){ progress++; publishProgress(progress); try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return null; } @Override protected void onPostExecute(Void result) { if(mStatusCallback != null) mStatusCallback.onPostExecute(); } @Override protected void onProgressUpdate(Integer... values) { if(mStatusCallback != null) mStatusCallback.onProgressUpdate(values[0]); } @Override protected void onCancelled(Void result) { if(mStatusCallback != null) mStatusCallback.onCancelled(); } } |
Define helper functions to start, cancel and updating status of AsyncTask.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public void startBackgroundTask() { if (!isTaskExecuting) { mBackgroundTask = new BackgroundTask(); mBackgroundTask.execute(); isTaskExecuting = true; } } public void cancelBackgroundTask() { if (isTaskExecuting) { mBackgroundTask.cancel(true); isTaskExecuting = false; } } public void updateExecutingStatus(boolean isExecuting) { this.isTaskExecuting = isExecuting; } |
Final code for HeadlessFragment.java will be as below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
package com.androidsrc.headlessfragment; import android.app.Activity; import android.app.Fragment; import android.os.AsyncTask; import android.os.Bundle; public class HeadlessFragment extends Fragment { public static final String TAG_HEADLESS_FRAGMENT = "headless_fragment"; public static interface TaskStatusCallback { void onPreExecute(); void onProgressUpdate(int progress); void onPostExecute(); void onCancelled(); } TaskStatusCallback mStatusCallback; BackgroundTask mBackgroundTask; boolean isTaskExecuting = false; /** * Called when a fragment is first attached to its activity. * onCreate(Bundle) will be called after this. */ @Override public void onAttach(Activity activity) { super.onAttach(activity); mStatusCallback = (TaskStatusCallback) activity; } /** * Called to do initial creation of a fragment. * This is called after onAttach(Activity) and before onCreateView(LayoutInflater, ViewGroup, Bundle) */ @Override public void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setRetainInstance(true); } /** * Called when the fragment is no longer attached to its activity. This is called after onDestroy(). */ @Override public void onDetach() { // TODO Auto-generated method stub super.onDetach(); mStatusCallback = null; } private class BackgroundTask extends AsyncTask<Void, Integer, Void> { @Override protected void onPreExecute() { if (mStatusCallback != null) mStatusCallback.onPreExecute(); } @Override protected Void doInBackground(Void... params) { int progress = 0; while (progress < 100 && !isCancelled()) { progress++; publishProgress(progress); try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return null; } @Override protected void onPostExecute(Void result) { if (mStatusCallback != null) mStatusCallback.onPostExecute(); } @Override protected void onProgressUpdate(Integer... values) { if (mStatusCallback != null) mStatusCallback.onProgressUpdate(values[0]); } @Override protected void onCancelled(Void result) { if (mStatusCallback != null) mStatusCallback.onCancelled(); } } public void startBackgroundTask() { if (!isTaskExecuting) { mBackgroundTask = new BackgroundTask(); mBackgroundTask.execute(); isTaskExecuting = true; } } public void cancelBackgroundTask() { if (isTaskExecuting) { mBackgroundTask.cancel(true); isTaskExecuting = false; } } public void updateExecutingStatus(boolean isExecuting) { this.isTaskExecuting = isExecuting; } } |
5. In your application launcher activity, MainActivity.java implement View.OnClickListener for listening to view onclick events and TaskStatusCallback which we defined in HeadlessFragment.java to listen AsyncTask status. Override all the methods, Final code for MainActivity.java with basic handling of Button view click and UI update on callback from AsyncTask will be as below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
package com.androidsrc.headlessfragment; import android.app.Activity; import android.app.FragmentManager; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.androidsrc.headlessfragment.HeadlessFragment.TaskStatusCallback; public class MainActivity extends Activity implements TaskStatusCallback, OnClickListener { private HeadlessFragment mFragment; private ProgressBar mProgressBar; private TextView mProgressvalue; /** * Called when activity is starting. Most initialization part is done here. */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mProgressBar = (ProgressBar) findViewById(R.id.progressBar); mProgressvalue = (TextView) findViewById(R.id.progressValue); if (savedInstanceState != null) { int progress = savedInstanceState.getInt("progress_value"); mProgressvalue.setText(progress + "%"); mProgressBar.setProgress(progress); } FragmentManager mMgr = getFragmentManager(); mFragment = (HeadlessFragment) mMgr .findFragmentByTag(HeadlessFragment.TAG_HEADLESS_FRAGMENT); if (mFragment == null) { mFragment = new HeadlessFragment(); mMgr.beginTransaction() .add(mFragment, HeadlessFragment.TAG_HEADLESS_FRAGMENT) .commit(); } } /** * This method is called before an activity may be killed Store info in * bundle if required. */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("progress_value", mProgressBar.getProgress()); } // Background task Callbacks @Override public void onPreExecute() { Toast.makeText(getApplicationContext(), "onPreExecute", Toast.LENGTH_SHORT).show(); } @Override public void onPostExecute() { Toast.makeText(getApplicationContext(), "onPostExecute", Toast.LENGTH_SHORT).show(); if (mFragment != null) mFragment.updateExecutingStatus(false); } @Override public void onCancelled() { Toast.makeText(getApplicationContext(), "onCancelled", Toast.LENGTH_SHORT).show(); } @Override public void onProgressUpdate(int progress) { mProgressvalue.setText(progress + "%"); mProgressBar.setProgress(progress); } /** * Called when a view has been clicked */ @Override public void onClick(View v) { int viewId = v.getId(); switch (viewId) { case R.id.start: if (mFragment != null) mFragment.startBackgroundTask(); break; case R.id.cancel: if (mFragment != null) mFragment.cancelBackgroundTask(); break; case R.id.recreate: /** * Cause this Activity to be recreated with a new instance. This * results in essentially the same flow as when the Activity is * created due to a configuration change the current instance will * go through its lifecycle to onDestroy and a new instance then * created after it. */ recreate(); break; } } } |
6. Build and run your android application project, Observe that it has resolved both of the problems stated earlier for Android AsyncTask Configuration Change.