The problem
Suppose you have an
AsyncTask performing some task in the background (downloading data
from a web service, updating an online database, etc.). If the user
rotates the device,
as you probably know, the current Activity is destroyed
and recreated (the callback methods
onDestroy and onCreate are invoked).
As a result, when
the AsyncTask finishes its task and should deliver its results, the
original Activity may no longer exist
or it could have been replaced by a brand new Activity. However the
AsyncTask is tied to the original Activity and its onPostExecute
method won’t work with the newly created Activity. The newly
created Activity may even start another AsyncTask (like in our
example, see below), thus wasting system resources.
That’s why, as a
general rule, you should always perform
network operations in a Service: the
Service’s lifecycle isn’t directly tied to the Activity
lifecycle; a Service is not destroyed and recreated on configuration
changes, like AsyncTasks do.
However AsyncTasks
are generally easier and quicker to program than Services, so let’s
examine some possibile solutions
to our “configuration changes problem” using AsyncTasks.Code
You can download the code for this tutorial from the Android – The Techinal Blog Githubrepository.The easy solution
A very easy solution
to our problem is preventing the
Activity from being destroyed and recreated
on configuration changes. For example, if you use the same layout for
portrait and landscape orientation there is no need to destroy and
recreate the Activity when the user rotates the device.
To achieve this,
just insert the following line in the AndroidManifest.xml
file under the Activity tag:
android:configChanges="orientation|screenSize|keyboardHidden”
This way, when the
user rotates the device, the Activity is not destroyed and recreated
anymore; conversely the method onConfigurationChanged
gets called and you can override it if you need it.
The correct solution
The correct solution
to the “configuration change problem” is using a so called
“retained Fragment”.
A Fragment
represents a behavior or a portion of the user interface within an
Activity. Fragment’s life cycle is generally coordinated
with the lifecycle of its containing Activity:
when the Activity is destroyed on configuration changes all the
contained Fragments get destroyed too.
However if you call
setRetainInstance(true)
within the Fragment class the destroy-and-recreated cycle of the
Fragment is bypassed. As a result you can use a “retained Fragment”
to hold active objects, like the AsyncTasks, Sockets, etc.
Let’s start with the
retained Fragment:
/**
* This Fragment performs a specific background task and is not
* destroyed on configuration changes.
*/
public class RetainedFragment extends Fragment {
/**
* Interface used by the retained Fragment to communicate with
* the Activity.
*/
interface RetainedFragmentCallbacks {
void onPreExecute();
void onProgressUpdate(int progress);
void onCancelled();
void onPostExecute();
}
private RetainedFragmentCallbacks retainedFragmentCallbacks;
private MyAsyncTask myAsyncTask;
/**
* Hold a reference to the parent Activity so we can report the
* task's current progress and results (the Activity must implement
* the RetainedFragmentCallbacks interface to receive the data sent
* by this Fragment).
* On configuration changes the Android framework passes to this
* method a reference to the newly created Activity. This way
* the AsyncTask can deliver its results to the new Activity.
*/
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
retainedFragmentCallbacks = (RetainedFragmentCallbacks) activity;
}
/**
* This method gets invoked when the Fragment is created for
* first time.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// This Fragment won’t be destroyed on configuration changes.
setRetainInstance(true);
// Create and execute the background task.
myAsyncTask = new MyAsyncTask();
myAsyncTask.execute();
}
/**
* Set the callback to null so we don't accidentally leak the
* Activity instance.
*/
@Override
public void onDetach() {
super.onDetach();
retainedFragmentCallbacks = null;
}
/**
* This AsyncTask performs a long task in the background
* (we fake it with a sleep) and communicates progress and
* results to the containing Activity.
* In each method we check that retainedFragmentCallbacks is
* not null because the Activity may have been destroyed on
* configuration changes or because the user exits the Activity
* pressing the back button.
*/
private class MyAsyncTask extends AsyncTask {
@Override
protected void onPreExecute() {
if (retainedFragmentCallbacks != null) {
retainedFragmentCallbacks.onPreExecute();
}
}
/**
* Note that we do NOT call the callback object's methods
* directly from the background thread, as this could result
* in a race condition.
*/
@Override
protected Void doInBackground(Void... ignore) {
for (int i = 0; !isCancelled() && i < 10; i++) {
SystemClock.sleep(1000);
publishProgress(i);
}
return null;
}
@Override
protected void onProgressUpdate(Integer... progress) {
if (retainedFragmentCallbacks != null) {
retainedFragmentCallbacks.onProgressUpdate(progress[0]);
}
}
@Override
protected void onCancelled() {
if (retainedFragmentCallbacks != null) {
retainedFragmentCallbacks.onCancelled();
}
}
@Override
protected void onPostExecute(Void ignore) {
if (retainedFragmentCallbacks != null) {
retainedFragmentCallbacks.onPostExecute();
}
}
}
}
And now the code of the
main Activity:
/**
* This Activity creates a RetainedFragment to perform a background
* task, and receives progress updates and results from the
* Fragment when they occur.
*/
public class MainActivity extends AppCompatActivity implements RetainedFragment. RetainedFragmentCallbacks {
private static final String TAG_RETAINED_FRAGMENT = "RETAINED FRAGMENT";
private RetainedFragment retainedFragment;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.textView);
FragmentManager fm = getFragmentManager();
retainedFragment = (RetainedFragment) fm.findFragmentByTag(TAG_RETAINED_FRAGMENT);
// If the Fragment is not null, then it is currently being
// retained across a configuration change.
if (retainedFragment == null) {
retainedFragment = new RetainedFragment();
fm.beginTransaction().add(retainedFragment, TAG_RETAINED_FRAGMENT).commit();
Log.d(TAG_RETAINED_FRAGMENT, "Retained Fragment created anew");
}
else {
Log.d(TAG_RETAINED_FRAGMENT, "Retained Fragment retained on configuration change");
}
}
// The four methods below are called by the RetainedFragment when new
// progress updates or results are available. The MainActivity
// should respond by updating its UI to indicate the change.
@Override
public void onPreExecute() {
Log.d(TAG_RETAINED_FRAGMENT, "onPreExecute() received...");
textView.setText("onPreExecute() received...");
}
@Override
public void onProgressUpdate(int progress) {
Log.d(TAG_RETAINED_FRAGMENT, "onProgressUpdate received with progress: " + progress);
textView.setText("onProgressUpdate received with progress: " + progress);
}
@Override
public void onCancelled() {
Log.d(TAG_RETAINED_FRAGMENT, "onCancelled() received...");
textView.setText("onCancelled() received...");
}
@Override
public void onPostExecute() {
Log.d(TAG_RETAINED_FRAGMENT, "onPostExecute() received...");
textView.setText("onPostExecute() received...");
}
}
The code is quite
self-explanatory.
When the Activity is
created for the first time the method findFragmentByTag returns
null, since no Fragment has already
been created. As a result a new instance of the RetainedFragment
class is created and attached to the main Activity. The main Activity
implements RetainedFragment.RetainedFragmentCallbacks
interface and is registered to receive
updates from the Fragment in the method
onAttach.
When the Fragment is
created setRetainInstance(true) ensures that it
won’t be destroyed on configuration changes.
In addition to that the Fragment creates and starts an AsyncTask,
defined and managed inside the Fragment class, to perform a long
lasting task in the background (for example downloading data from a
web service).
Progress updates and
final results are delivered to the main Activity through the
RetainedFragment.RetainedFragmentCallbacks
interface, which is implemented in the Activity (see the logs).
Now let’s suppose that
the user rotates the device. This is what happens:
- the RetainedFragment is not destroyed (because we have called setRetainInstance(true) when the Fragment was created) and the AsyncTask continues to work in the background;
- the main Activity is destroyed and recreated as usual. When it’s destroyed it’s also detached from the RetainedFragment and the retainedFragmentCallbacks variable inside the Fragment class is set to null as a result. When the Activity is recreated it’s re-attached to the existing Fragment and the retainedFragmentCallbacks variable inside the Fragment is now set to point to the newly created Activity: results and progress updates are therefore delivered to this new Activity, and not to the old one (which doesn’t exist anymore). Notice that the RetainedFragment itself is not recreated because the method findFragmentByTag in onCreate now returns the existing Fragment.