⬆️ ⬇️

Fragment transactions and loss of activity

For those who have not encountered this problem, I will explain with an example - at the end of a lengthy background operation you show a dialogue (yes, Google does not recommend doing so, but the customer requires it). If, before displaying the dialog, you minimize the application by pressing the Home key, then the IllegalStateException will be thrown while the dialog is displayed. The same will happen if the waiting dialog is shown and hidden after the background activity is completed - calling the dismiss () method after saving the state will cause an exception.







The best article on this topic that I found to google the problem is the Fragment Transactions & Activity State Loss . The article explains the problem, but gives only general advice, the problem itself remains unresolved. Perhaps some of the habragers would be interested in translating the article, but for now I’ll tell you in brief its meaning. The Android system has the ability to complete any activity of your application and its fragments when there is a shortage of memory. To hide this unfortunate fact from the user, Android saves the state of activity and restores it if necessary, so that the user does not even notice which cataclysms occurred at the code level. When you try to display a dialog after saving a state, in essence you break the saved state and such activity cannot be restored. Android solves this in the simplest way for itself - throws an exception and does not allow committing a transaction of fragments. And your application just crashes.

')

There are lots of ways to deal with this problem, they all boil down to postponing the transaction until the moment of recovery, namely in the Activity.onPostResume or Fragment.onResume functions. The first thing that comes to mind is that instead of showing the dialog, check the box, check it in onResume and display the dialog there. In the case of onActivityResult, this will even work - because this function is always called before the state of activity is restored. But in the case of background processing, you don’t even know what state the activity will be in, but there is simply no simple method for checking the activity state. Google's recommended way is to use the Loader for background processing. However, it is not always convenient. For example, their own Volley library does not use this template and there is no easy way to connect it.



I will not torment you with other unsuccessful attempts to get around the problem. I think many of you have some options, I will share my decision.



Let's write a test application:



public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startAsyncTask(); } private void startAsyncTask() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } runOnUiThread(new Runnable() { @Override public void run() { showMyDialog(); } }); return null; } }.execute(); } private void showMyDialog() { new TestDialog().show(getSupportFragmentManager(), "dialog"); } public class TestDialog extends DialogFragment { public TestDialog(){} @Override public Dialog onCreateDialog (Bundle savedInstanceState){ AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setMessage("Hello World"); builder.setPositiveButton("OK", null); return builder.create(); } } } 




If you run it, it will display a dialog in three seconds. But if, before the expiration of three seconds, you press the Home key, then when the dialog is displayed, the application will crash.



Now add the following class, it will track the activity status for us:



 public class StateHandler { /** *   Runnable */ private final List<Runnable> queueBuffer = Collections.synchronizedList(new ArrayList<Runnable>()); /** * ,    */ private Activity activity; /** *   */ public final synchronized void resume(Activity activity) { this.activity = activity; while (queueBuffer.size() > 0) { final Runnable runnable = queueBuffer.get(0); queueBuffer.remove(0); runnable.run(); } } /** *   */ public final synchronized void pause() { activity = null; } /** *  Runnable     ,      * * @param runnable  Runnable,   . */ public final synchronized void run(Runnable runnable) { if (activity == null) { queueBuffer.add(runnable); } else { runnable.run(); } } } 




In order not to repeat the routine actions on initialization and support of the state processing class, we will render their basic activity:



 public class BaseActivity extends ActionBarActivity { protected StateHandler stateHandler = new StateHandler(); @Override protected void onPause() { super.onPause(); stateHandler.pause(); } @Override protected void onPostResume() { super.onPostResume(); stateHandler.resume(this); } } 




All that remains to be done is to wrap the transaction using our new state handler:



  stateHandler.run(new Runnable() { @Override public void run() { new TestDialog().show(getSupportFragmentManager(), "dialog"); } }); 




In case the state allows, the code inside Runnable will be immediately executed. Otherwise, the code will be placed in the queue and will be executed after the restoration of activity.



In the same way, you can use this class inside fragments, only instead of the onPostResume method, call the handler code from the onResume method of the base fragment.



The source code of the example can be found on github .

Source: https://habr.com/ru/post/247561/



All Articles