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 acheiving 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

 

[su_button url=”https://github.com/androidsrc/HeadlessFragment” target=”blank” style=”stroked” background=”#51d461″ color=”#ffffff” size=”6″ center=”yes” radius=”0″ icon=”icon: arrow-circle-o-down”]Download Complete Source Code[/su_button]

 

Headless_Fragment_Hanlde_Orientation

 

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.

<?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.

<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.

	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.

	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.

	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.

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.

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.

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.

You may also like...

4 Responses

  1. July 10, 2015

    […] using single layout only. If that is the case, then this is best solution. But, if not then try this, which basically is Headless Fragment as pointed by […]

  2. September 9, 2015

    […] I have been trying to develop an in app billing feature in my app and even though using headless fragment to actual do the in app billing, makes more sense after seeing this – http://cs2guru.com/handle-android-asynctask-configuration-change-using-fragment/ […]

  3. September 9, 2015

    […] I have been trying to develop an in app billing feature in my app and even though using headless fragment to actual do the in app billing, makes more sense after seeing this – http://cs2guru.com/handle-android-asynctask-configuration-change-using-fragment/ […]

Leave a Reply

Your email address will not be published. Required fields are marked *