Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Fragments. Retaining an AsyncTask during screen rotation or configuration change

I'm working on a Smartphone / Tablet app, using only one APK, and loading resources as is needed depending on screen size, the best design choice seemed to be using Fragments via the ACL.

This app has been working fine until now being only activity based. This is a mock class of how I handle AsyncTasks and ProgressDialogs in the Activities in order to have them work even when the screen is rotated or a configuration change occurs mid communication.

I will not change the manifest to avoid recreation of the Activity, there are many reasons why I dont want to do it, but mainly because the official docs say it isnt recomended and I've managed without it this far, so please dont recomend that route.

public class Login extends Activity {      static ProgressDialog pd;     AsyncTask<String, Void, Boolean> asyncLoginThread;      @Override     public void onCreate(Bundle icicle) {         super.onCreate(icicle);         setContentView(R.layout.login);         //SETUP UI OBJECTS         restoreAsyncTask();     }      @Override     public Object onRetainNonConfigurationInstance() {         if (pd != null) pd.dismiss();         if (asyncLoginThread != null) return (asyncLoginThread);         return super.onRetainNonConfigurationInstance();     }      private void restoreAsyncTask();() {         pd = new ProgressDialog(Login.this);         if (getLastNonConfigurationInstance() != null) {             asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();             if (asyncLoginThread != null) {                 if (!(asyncLoginThread.getStatus()                         .equals(AsyncTask.Status.FINISHED))) {                     showProgressDialog();                 }             }         }     }      public class LoginThread extends AsyncTask<String, Void, Boolean> {         @Override         protected Boolean doInBackground(String... args) {             try {                 //Connect to WS, recieve a JSON/XML Response                 //Place it somewhere I can use it.             } catch (Exception e) {                 return true;             }             return true;         }          protected void onPostExecute(Boolean result) {             if (result) {                 pd.dismiss();                 //Handle the response. Either deny entry or launch new Login Succesful Activity             }         }     } } 

This code is working fine, I have around 10.000 users without complaint, so it seemed logical to just copy this logic into the new Fragment Based Design, but, of course, it isnt working.

Here is the LoginFragment:

public class LoginFragment extends Fragment {      FragmentActivity parentActivity;     static ProgressDialog pd;     AsyncTask<String, Void, Boolean> asyncLoginThread;      public interface OnLoginSuccessfulListener {         public void onLoginSuccessful(GlobalContainer globalContainer);     }      public void onSaveInstanceState(Bundle outState){         super.onSaveInstanceState(outState);         //Save some stuff for the UI State     }      @Override     public void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         //setRetainInstance(true);         //If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.         parentActivity = getActivity();     }      @Override     public void onAttach(Activity activity) {         super.onAttach(activity);         try {             loginSuccessfulListener = (OnLoginSuccessfulListener) activity;         } catch (ClassCastException e) {             throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");         }     }      @Override     public View onCreateView(LayoutInflater inflater, ViewGroup container,             Bundle savedInstanceState) {         RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);         return loginLayout;     }      @Override     public void onActivityCreated(Bundle savedInstanceState) {         super.onActivityCreated(savedInstanceState);         //SETUP UI OBJECTS         if(savedInstanceState != null){             //Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.         }     }      public class LoginThread extends AsyncTask<String, Void, Boolean> {             @Override             protected Boolean doInBackground(String... args) {                 try {                     //Connect to WS, recieve a JSON/XML Response                     //Place it somewhere I can use it.                 } catch (Exception e) {                     return true;                 }                 return true;             }              protected void onPostExecute(Boolean result) {                 if (result) {                     pd.dismiss();                     //Handle the response. Either deny entry or launch new Login Succesful Activity                 }             }         }     } } 

I cant use onRetainNonConfigurationInstance() since it has to be called from the Activity and not the Fragment, same goes with getLastNonConfigurationInstance(). I've read some similar questions here with no answer.

I understand that it might require some working around to get this stuff organized properly in fragments, that being said, I would like to maintain the same basic design logic.

What would be the proper way to retain the AsyncTask during a configuration change, and if its still runing, show a progressDialog, taking into consideration that the AsyncTask is a inner class to the Fragment and it is the Fragment itself who invokes the AsyncTask.execute()?

like image 762
blindstuff Avatar asked Dec 07 '11 15:12

blindstuff


People also ask

What will happen if an activity with a retained fragment is rotated?

Fragments — Scenario 3: Activity with retained Fragment is rotated. The fragment is not destroyed nor created after the rotation because the same fragment instance is used after the activity is recreated. The state bundle is still available in onActivityCreated .

What is the problem with AsyncTask in Android?

In summary, the three most common issues with AsyncTask are: Memory leaks. Cancellation of background work. Computational cost.

What is the best way to retain active objects such as running threads?

Ever since the introduction of Fragments in Android 3.0, the recommended means of retaining active objects across Activity instances is to wrap and manage them inside of a retained “worker” Fragment. By default, Fragments are destroyed and recreated along with their parent Activitys when a configuration change occurs.


2 Answers

Fragments can actually make this a lot easier. Just use the method Fragment.setRetainInstance(boolean) to have your fragment instance retained across configuration changes. Note that this is the recommended replacement for Activity.onRetainnonConfigurationInstance() in the docs.

If for some reason you really don't want to use a retained fragment, there are other approaches you can take. Note that each fragment has a unique identifier returned by Fragment.getId(). You can also find out if a fragment is being torn down for a config change through Fragment.getActivity().isChangingConfigurations(). So, at the point where you would decide to stop your AsyncTask (in onStop() or onDestroy() most likely), you could for example check if the configuration is changing and if so stick it in a static SparseArray under the fragment's identifier, and then in your onCreate() or onStart() look to see if you have an AsyncTask in the sparse array available.

like image 75
hackbod Avatar answered Oct 13 '22 01:10

hackbod


I think you will enjoy my extremely comprehensive and working example detailed below.

  1. Rotation works, and the dialog survives.
  2. You can cancel the task and dialog by pressing the back button (if you want this behaviour).
  3. It uses fragments.
  4. The layout of the fragment underneath the activity changes properly when the device rotates.
  5. There is a complete source code download and a precompiled APK so you can see if the behaviour is what you want.

Edit

As requested by Brad Larson I have reproduced most of the linked solution below. Also since I posted it I have been pointed to AsyncTaskLoader. I'm not sure it is totally applicable to the same problems, but you should check it out anyway.

Using AsyncTask with progress dialogs and device rotation.

A working solution!

I have finally got everything to work. My code has the following features:

  1. A Fragment whose layout changes with orientation.
  2. An AsyncTask in which you can do some work.
  3. A DialogFragment which shows the progress of the task in a progress bar (not just an indeterminate spinner).
  4. Rotation works without interrupting the task or dismissing the dialog.
  5. The back button dismisses the dialog and cancels the task (you can alter this behaviour fairly easily though).

I don't think that combination of workingness can be found anywhere else.

The basic idea is as follows. There is a MainActivity class which contains a single fragment - MainFragment. MainFragment has different layouts for horizontal and vertical orientation, and setRetainInstance() is false so that the layout can change. This means that when the device orientation is changed, both MainActivity and MainFragment are completely destroyed and recreated.

Separately we have MyTask (extended from AsyncTask) which does all the work. We can't store it in MainFragment because that will be destroyed, and Google has deprecated using anything like setRetainNonInstanceConfiguration(). That isn't always available anyway and is an ugly hack at best. Instead we will store MyTask in another fragment, a DialogFragment called TaskFragment. This fragment will have setRetainInstance() set to true, so as the device rotates this fragment isn't destroyed, and MyTask is retained.

Finally we need to tell the TaskFragment who to inform when it is finished, and we do that using setTargetFragment(<the MainFragment>) when we create it. When the device is rotated and the MainFragment is destroyed and a new instance is created, we use the FragmentManager to find the dialog (based on its tag) and do setTargetFragment(<the new MainFragment>). That's pretty much it.

There were two other things I needed to do: first cancel the task when the dialog is dismissed, and second set the dismiss message to null, otherwise the dialog is weirdly dismissed when the device is rotated.

The code

I won't list the layouts, they are pretty obvious and you can find them in the project download below.

MainActivity

This is pretty straightforward. I added a callback into this activity so it knows when the task is finished, but you might not need that. Mainly I just wanted to show the fragment-activity callback mechanism because it's quite neat and you might not have seen it before.

public class MainActivity extends Activity implements MainFragment.Callbacks {     @Override     public void onCreate(Bundle savedInstanceState)     {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);     }     @Override     public void onTaskFinished()     {         // Hooray. A toast to our success.         Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();         // NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*         // the duration in milliseconds. ANDROID Y U NO ENUM?      } } 

MainFragment

It's long but worth it!

public class MainFragment extends Fragment implements OnClickListener {     // This code up to onDetach() is all to get easy callbacks to the Activity.      private Callbacks mCallbacks = sDummyCallbacks;      public interface Callbacks     {         public void onTaskFinished();     }     private static Callbacks sDummyCallbacks = new Callbacks()     {         public void onTaskFinished() { }     };      @Override     public void onAttach(Activity activity)     {         super.onAttach(activity);         if (!(activity instanceof Callbacks))         {             throw new IllegalStateException("Activity must implement fragment's callbacks.");         }         mCallbacks = (Callbacks) activity;     }      @Override     public void onDetach()     {         super.onDetach();         mCallbacks = sDummyCallbacks;     }      // Save a reference to the fragment manager. This is initialised in onCreate().     private FragmentManager mFM;      // Code to identify the fragment that is calling onActivityResult(). We don't really need     // this since we only have one fragment to deal with.     static final int TASK_FRAGMENT = 0;      // Tag so we can find the task fragment again, in another instance of this fragment after rotation.     static final String TASK_FRAGMENT_TAG = "task";      @Override     public void onCreate(Bundle savedInstanceState)     {         super.onCreate(savedInstanceState);          // At this point the fragment may have been recreated due to a rotation,         // and there may be a TaskFragment lying around. So see if we can find it.         mFM = getFragmentManager();         // Check to see if we have retained the worker fragment.         TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);          if (taskFragment != null)         {             // Update the target fragment so it goes to this fragment instead of the old one.             // This will also allow the GC to reclaim the old MainFragment, which the TaskFragment             // keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't             // use weak references. To be sure you aren't leaking, you may wish to make your own             // setTargetFragment() which does.             taskFragment.setTargetFragment(this, TASK_FRAGMENT);         }     }      @Override     public View onCreateView(LayoutInflater inflater, ViewGroup container,             Bundle savedInstanceState)     {         return inflater.inflate(R.layout.fragment_main, container, false);     }      @Override     public void onViewCreated(View view, Bundle savedInstanceState)     {         super.onViewCreated(view, savedInstanceState);          // Callback for the "start task" button. I originally used the XML onClick()         // but it goes to the Activity instead.         view.findViewById(R.id.taskButton).setOnClickListener(this);     }      @Override     public void onClick(View v)     {         // We only have one click listener so we know it is the "Start Task" button.          // We will create a new TaskFragment.         TaskFragment taskFragment = new TaskFragment();         // And create a task for it to monitor. In this implementation the taskFragment         // executes the task, but you could change it so that it is started here.         taskFragment.setTask(new MyTask());         // And tell it to call onActivityResult() on this fragment.         taskFragment.setTargetFragment(this, TASK_FRAGMENT);          // Show the fragment.         // I'm not sure which of the following two lines is best to use but this one works well.         taskFragment.show(mFM, TASK_FRAGMENT_TAG); //      mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();     }      @Override     public void onActivityResult(int requestCode, int resultCode, Intent data)     {         if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)         {             // Inform the activity.              mCallbacks.onTaskFinished();         }     } 

TaskFragment

    // This and the other inner class can be in separate files if you like.     // There's no reason they need to be inner classes other than keeping everything together.     public static class TaskFragment extends DialogFragment     {         // The task we are running.         MyTask mTask;         ProgressBar mProgressBar;          public void setTask(MyTask task)         {             mTask = task;              // Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.             mTask.setFragment(this);         }          @Override         public void onCreate(Bundle savedInstanceState)         {             super.onCreate(savedInstanceState);              // Retain this instance so it isn't destroyed when MainActivity and             // MainFragment change configuration.             setRetainInstance(true);              // Start the task! You could move this outside this activity if you want.             if (mTask != null)                 mTask.execute();         }          @Override         public View onCreateView(LayoutInflater inflater, ViewGroup container,                 Bundle savedInstanceState)         {             View view = inflater.inflate(R.layout.fragment_task, container);             mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);              getDialog().setTitle("Progress Dialog");              // If you're doing a long task, you probably don't want people to cancel             // it just by tapping the screen!             getDialog().setCanceledOnTouchOutside(false);              return view;         }          // This is to work around what is apparently a bug. If you don't have it         // here the dialog will be dismissed on rotation, so tell it not to dismiss.         @Override         public void onDestroyView()         {             if (getDialog() != null && getRetainInstance())                 getDialog().setDismissMessage(null);             super.onDestroyView();         }          // Also when we are dismissed we need to cancel the task.         @Override         public void onDismiss(DialogInterface dialog)         {             super.onDismiss(dialog);             // If true, the thread is interrupted immediately, which may do bad things.             // If false, it guarantees a result is never returned (onPostExecute() isn't called)             // but you have to repeatedly call isCancelled() in your doInBackground()             // function to check if it should exit. For some tasks that might not be feasible.             if (mTask != null) {                 mTask.cancel(false);             }              // You don't really need this if you don't want.             if (getTargetFragment() != null)                 getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);         }          @Override         public void onResume()         {             super.onResume();             // This is a little hacky, but we will see if the task has finished while we weren't             // in this activity, and then we can dismiss ourselves.             if (mTask == null)                 dismiss();         }          // This is called by the AsyncTask.         public void updateProgress(int percent)         {             mProgressBar.setProgress(percent);         }          // This is also called by the AsyncTask.         public void taskFinished()         {             // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog             // after the user has switched to another app.             if (isResumed())                 dismiss();              // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in             // onResume().             mTask = null;              // Tell the fragment that we are done.             if (getTargetFragment() != null)                 getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);         }     } 

MyTask

    // This is a fairly standard AsyncTask that does some dummy work.     public static class MyTask extends AsyncTask<Void, Void, Void>     {         TaskFragment mFragment;         int mProgress = 0;          void setFragment(TaskFragment fragment)         {             mFragment = fragment;         }          @Override         protected Void doInBackground(Void... params)         {             // Do some longish task. This should be a task that we don't really             // care about continuing             // if the user exits the app.             // Examples of these things:             // * Logging in to an app.             // * Downloading something for the user to view.             // * Calculating something for the user to view.             // Examples of where you should probably use a service instead:             // * Downloading files for the user to save (like the browser does).             // * Sending messages to people.             // * Uploading data to a server.             for (int i = 0; i < 10; i++)             {                 // Check if this has been cancelled, e.g. when the dialog is dismissed.                 if (isCancelled())                     return null;                  SystemClock.sleep(500);                 mProgress = i * 10;                 publishProgress();             }             return null;         }          @Override         protected void onProgressUpdate(Void... unused)         {             if (mFragment == null)                 return;             mFragment.updateProgress(mProgress);         }          @Override         protected void onPostExecute(Void unused)         {             if (mFragment == null)                 return;             mFragment.taskFinished();         }     } } 

Download the example project

Here is the source code and the APK. Sorry, the ADT insisted on adding the support library before it would let me make a project. I'm sure you can remove it.

like image 43
Timmmm Avatar answered Oct 13 '22 00:10

Timmmm