This is a followup question to these questions:
popBackStack() after saveInstanceState()
Application crashes in background, when popping a fragment from stack
I am creating an application which uses a service and is reacting to events which are created by the service. One of the events is called within a fragment and is popping from the backstack like this:
getSupportFragmentManager().popBackStack(stringTag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
When the app is in the foreground it works fine. When the app is in the background, I get an
IllegalStateException: Can not perform this action after onSaveInstanceState
I have already tried overriding onSaveInstanceState with an empty method.
Why do I get this exception only when the app is in the background and how can I solve it?
Try something like this.
public abstract class PopActivity extends Activity {
private boolean mVisible;
#Override
public void onResume() {
super.onResume();
mVisible = true;
}
#Override
protected void onPause() {
super.onPause();
mVisible = false;
}
private void popFragment() {
if (!mVisible) {
return;
}
FragmentManager fm = getSupportFragmentManager();
fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
when you implement the above code alone when you resume the app you will find yourself in a fragment that you actually want to be popped. You can use the following snipped to fix this issue:
public abstract class PopFragment extends Fragment {
private static final String KEY_IS_POPPED = "KEY_IS_POPPED";
private boolean mPopped;
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(KEY_IS_POPPED, mPopped);
super.onSaveInstanceState(outState);
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mPopped = savedInstanceState.getBoolean(KEY_IS_POPPED);
}
}
#Override
public void onResume() {
super.onResume();
if (mPopped) {
popFragment();
}
}
protected void popFragment() {
mPopped = true;
// null check and interface check advised
((PopActivity) getActivity()).popFragment();
}
}
Original Author
Related
This question already has answers here:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
(17 answers)
Closed 5 years ago.
I'm getting a following error when trying to replace a fragment upon receiving a response from AsyncTask:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
The thing is, I get this error randomly upon restarting my app through Android Studio. In a simplified version my activity contains 4 key methods (onCreate, taskCompleted, parseJSON and fragmentReplace), that determine which fragment should the user see at the start:
private AsyncTask mMyTask;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMyTask = new AsyncTask(this, this);
mMyTask.executeTaskCall("check_user");
}
#Override
public void taskCompleted(String results) {
try {
JSONObject jsonBody = new JSONObject(results);
parseJSON(jsonBody);
}
catch (JSONException je){
}
}
private void parseJSON(JSONObject jsonBody) throws JSONException {
boolean userActive = jsonBody.getBoolean("result");
if (userActive){
fragmentReplace(new FirstFragment(), "FirstFragment");
}
else {
fragmentReplace(new SecondFragment(), "SecondFragment");
}
}
public void fragmentReplace(Fragment fragment, String fragmentTag){
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.layout_container, fragment, fragmentTag)
.commit();
}
What is the reason of this exception happening so random?
You should read WeakReference solution (or may be other also) at java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState.
There is one alternate solution for this problem. Using flag you can handle it, like below
/**
* Flag to avoid "java.lang.IllegalStateException: Can not perform this action after
* onSaveInstanceState". Avoid Fragment transaction until onRestoreInstanceState or onResume
* gets called.
*/
private boolean isOnSaveInstanceStateCalled = false;
#Override
public void onRestoreInstanceState(final Bundle bundle) {
.....
isOnSaveInstanceStateCalled = false;
.....
}
#Override
public void onSaveInstanceState(final Bundle outState) {
.....
isOnSaveInstanceStateCalled = true;
.....
}
#Override
public void onResume() {
super.onResume();
isOnSaveInstanceStateCalled = false;
.....
}
And you can check this boolean value while doing fragment transaction.
private void fragmentReplace(Fragment fragment, String fragmentTag){
if (!isOnSaveInstanceStateCalled) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.layout_container, fragment, fragmentTag)
.commit();
}
}
Update for API 26.1+ (contributed by Stephen M)
Fragment.isStateSaved() has been added since 26.1.0, which can also be used for same purpose.
I have an Activity which has a Save details button and a Viewpager which contains 4 fragments. The Fragments contains User details form. Once the Save button is clicked I need to get the data from all fragments and save the details. How to get the data entered by the user in all 4 fragments when Save button is clicked in the activity?
I just worked on an app that had the same use case. In addition, I had to save the data on a back navigation as well. The problem was a bit more difficult that I though it should have been. The problems came from the fact that not all the fragments in the ViewPager are guaranteed to be alive. They either may not have been started yet, or destroyed when the user paged off of them.
To solve the problem, I took inspiration from this blog post about handing back-press events. I had to modify it a bit to allow for any fragments that may be running and not just one.
public abstract class BackHandledFragment extends Fragment {
protected BackHandlerInterface backHandlerInterface;
public abstract String getTagText();
public abstract boolean onBackPressed();
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(!(getActivity() instanceof BackHandlerInterface)) {
throw new ClassCastException("Hosting activity must implement BackHandlerInterface");
} else {
backHandlerInterface = (BackHandlerInterface) getActivity();
}
}
#Override
public void onResume() {
super.onResume();
backHandlerInterface.addRunningFragment(this);
}
#Override
public void onPause() {
super.onPause();
backHandlerInterface.removeRunningFragment(this);
}
public interface BackHandlerInterface {
public void addRunningFragment(BackHandledFragment backHandledFragment);
public void removeRunningFragment(BackHandledFragment backHandledFragment);
}
}
The Activity implements the interface and tracks the active fragments:
public class EditActivity implements BackHandledFragment.BackHandlerInterface
{
private List<BackHandledFragment> listActiveFragments = new ArrayList<>
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Notify each active fragment that the back was pressed, this will allow
// them to save any data.
for (BackHandledFragment bf : listActiveFragments) {
bf.onBackPressed();
}
}
#Override
public void addRunningFragment(BackHandledFragment backHandledFragment) {
listActiveFragments.add(backHandledFragment);
}
#Override
public void removeRunningFragment(BackHandledFragment backHandledFragment) {
listActiveFragments.remove(backHandledFragment);
}();
}
Each fragment must extend BackHandledFragment:
public class DetailNetworkFragment extends BackHandledFragment {
#Override
public void onPause() {
super.onPause();
EventBus.getDefault().unregister(this);
saveDataFields();
}
#Override
public void onResume() {
super.onResume();
EventBus.getDefault().register(this);
}
#Override
public String getTagText() {
return TAG;
}
#Override
public boolean onBackPressed() {
saveDataFields();
return false;
}
}
The saveDataFields() is not too interesting. It just copies the data out of the UI views and saves them back to an object in the Activity.
I have an app that uses sensor listeners in fragments to detect shaking gestures on a Moto360 smartwatch. Each fragment has a different set of gestures so must register/unregister it's listeners from the sensor manager when it is opened/closed. Everything works fine until the watch goes into ambient mode. Once the app returns from ambient mode the sensors start to malfunction or stop working altogether. I suspect I may be handling the registering/unregistering incorrectly. Can anyone see where I might be going wrong?
I have a MainWearActivity class which extends WearableActivity and handles all the fragment transactions:
public class MainWearActivity extends WearableActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main_wear);
setAmbientEnabled();
// ... etc ...//
}
#Override
public void onEnterAmbient(Bundle ambientDetails) {
super.onEnterAmbient(ambientDetails);
}
#Override
public void onUpdateAmbient() {
super.onUpdateAmbient();
}
#Override
public void onExitAmbient() {
super.onExitAmbient();
}
// Method the main fragment uses to replace itself with a new fragment
public void replaceFragment(Fragment frag, String fragTag) {
mFragmentTransaction = mFragmentManager.beginTransaction();
mFragmentTransaction.replace(R.id.fragmentContainer, frag, fragTag);
mFragmentTransaction.addToBackStack(null);
mFragmentTransaction.commit();
}
}
I have a main fragment which presents a menu to the user and allows them to choose an option. When the user chooses an option, the main fragment is placed on the backstack and replaced by the option fragment. When the user returns from the option fragment, the backstack is popped to return to the main menu fragment:
public class MainFragment extends Fragment {
private MainWearActivity mMainWearActivity;
View view;
private SensorManager mSensorMgr;
private WearXFlick mXFlick; //Extends SensorEventListener
private Vibrator vibrator;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMainWearActivity = (MainWearActivity) getActivity();
vibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
mSensorMgr = (SensorManager) mMainWearActivity.getSystemService(Activity.SENSOR_SERVICE);
mXFlick = new WearXFlick();
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mSensorMgr.registerListener(mXFlick, mSensorMgr.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_GAME);
mXFlick.setOnShakeListener(new OnShakeListener() {
#Override
public void onShake() {
vibrator.vibrate(Consts.vibPattern, -1);
// Select the current menu item
switch (mMainWearActivity.getCurrentSelection()) {
case Consts.MENU_MAIL:
MailFragment mf = new MailFragment();
mMainWearActivity.replaceFragment(mf, Consts.FRAG_MAIL);
break;
}
}
});
}
#Override
public void onStop() {
mSensorMgr.unregisterListener(mXFlick);
super.onStop();
}
#Override
public void onStart() {
super.onStart();
mSensorMgr.registerListener(mXFlick, mSensorMgr.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_GAME);
}
#Override
public void onPause() {
mSensorMgr.unregisterListener(mXFlick);
super.onPause();
}
#Override
public void onResume() {
super.onResume();
mSensorMgr.registerListener(mXFlick, mSensorMgr.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_GAME);
}
#Override
public void onDestroyView() {
mSensorMgr.unregisterListener(mXFlick);
super.onDestroyView();
}
}
Once replaceFragment() is called, a new fragment is loaded which basically has the same format as this, where a different set of sensor listeners are registered and unregistered on stop/start/pause/resume, although I really don't know if that's the correct way I should be unregistering/registering the sensor listeners. It all works fine, though, until the app goes into ambient mode and returns. When that happens, the sensors malfunction or fail.
This is my first activity where Im making a post call. The bus provider is the default one in the otto sample app.
void openNextActivity()
{
manager.bus.post("Hi");
// Intent to my next Activity
}
This is my fragment in another activity where im subscribing for the data. The bus received is the same, however the subscribe method is not being called.
public class ProductListFragment extends BaseFragment {
String LOG_TAG = ProductListFragment.class.getCanonicalName();
public static ProductListFragment newInstance() {
ProductListFragment fragment = new ProductListFragment();
return fragment;
}
public ProductListFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActivity().invalidateOptionsMenu();
}
#Override
public void onResume() {
super.onResume();
BusProvider.getInstance().register(this);
}
#Override
public void onPause() {
super.onPause();
BusProvider.getInstance().unregister(this);
}
#Subscribe public void onPostRecived(String s) {
Log.d(LOG_TAG, s);
}
}
There are no errors on anything being received, however if I put a button onclick on the fragment and post some content from there, the subscribe method is being called. For eg.
#OnClick(R.id.makePostCall) void call() {
BusProvider.getInstance().post("Hi");
}
I'm getting the appropriate log on this call. Any idea where the code is going wrong?
it seems you subscribe your second activity's fragment after sending stuff to event bus. Consider changing your logic
u send msg before intent;the BusProvider id registered after intent;
just try:
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
BusProvider.getInstance().post("Hi");
}
},3000);
In my App the user has to login.
They open the app on the login page
They enter email/password and hit login
A LoadingScreenActivity is opened that has a swirly circle and is running an AsyncTask that goes to my database and retrieves all the users info
After the AsyncTask is completed it starts an intent to launch MainPageActivity.
There are two problems with this at the moment:
If the user logs in and then goes to the home screen while the app loads the MainPageActivity will open as soon as it is ready (on top of the existing home page) even though the app has been minimised
If the user logs in and then goes to the home screen while the app loads and then returns to the loading screen the AsyncTask will complete twice over
For problem 1. At the moment my onPostExecute() method in LoadingScreenActivity looks like this:
#Override
public void onPostExecute() {
//open the main page
Intent mainPage = new Intent(getApplicationContext(), MainPageActivity.class);
mainPage.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
mainPage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK );
startActivity(mainPage);
}
Is there a way I could detect in this method if the main page activity should be opened yet?
For problem 2. I've hit a complete road block on this, is there a way to detect if the activity has simply been re opened rather than started for the first time? I'd really appreciate any tips on this, I'm quite new to android so I'm not even convinced an Async task is the way to go with this.
Thanks for your time
LoadingScreenActivity.java
public class LoadingScreenActivity extends Activity implements TaskFragment.TaskCallbacks {
private static final String TAG_TASK_FRAGMENT = "task_fragment";
private TaskFragment mTaskFragment;
#Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
FragmentManager fm = getFragmentManager();
mTaskFragment = (TaskFragment) fm.findFragmentByTag(TAG_TASK_FRAGMENT);
// If the Fragment is non-null, then it is currently being
// retained across a configuration change.
if (mTaskFragment == null) {
mTaskFragment = new TaskFragment();
fm.beginTransaction().add(mTaskFragment, TAG_TASK_FRAGMENT).commit();
}
setContentView(R.layout.loading_screen);
ActionBar actionBar = getActionBar();
actionBar.hide();
TextView title = (TextView) findViewById(R.id.loading_title);
TextView progress = (TextView) findViewById(R.id.loading_progress);
title.setText(R.string.app_name);
progress.setText("Loading your info");
}
#Override
public Context onPreExecute() {
return getApplicationContext();
}
#Override
public void onProgressUpdate(int percent) {
}
#Override
public void onCancelled() {
Intent login = new Intent(getApplicationContext(), LoginActivity.class);
login.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(login);
finish();
}
#Override
public void onPostExecute() {
//open the main page
Intent mainPage = new Intent(getApplicationContext(), MainPageActivity.class);
mainPage.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
mainPage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK );
startActivity(mainPage);
}
}
and TaskFragment.java
public class TaskFragment extends Fragment {
static interface TaskCallbacks {
Context onPreExecute();
void onProgressUpdate(int percent);
void onCancelled();
void onPostExecute();
}
private TaskCallbacks mCallbacks;
private DummyTask mTask;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mCallbacks = (TaskCallbacks) activity;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain this fragment across configuration changes.
setRetainInstance(true);
// Create and execute the background task.
mTask = new DummyTask();
mTask.execute();
}
#Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
private class DummyTask extends AsyncTask<Void, Integer, Void> {
Context context;
boolean running = true;
#Override
protected void onPreExecute() {
if (mCallbacks != null) {
context = mCallbacks.onPreExecute();
}
}
#Override
protected Void doInBackground(Void... ignore) {
//Get the current thread's token
synchronized (this)
{
if(running){
DatabaseHandler dbHandler = new DatabaseHandler(context);
dbHandler.populateSQLiteDatabase();
}
}
return null;
}
#Override
protected void onProgressUpdate(Integer... percent) {
if (mCallbacks != null) {
mCallbacks.onProgressUpdate(percent[0]);
}
}
#Override
protected void onCancelled() {
if (mCallbacks != null) {
mCallbacks.onCancelled();
}
}
#Override
protected void onPostExecute(Void ignore) {
if (mCallbacks != null) {
mCallbacks.onPostExecute();
}
}
}
}
in your activity in the manifest just add android:configChanges="keyboardHidden|orientation|screenSize"
and in the activity implement this
#Override
public void onConfigurationChanged(Configuration newConfig) {
};
I'm afraid, i don't really understand your first problem.
About the second one, there are a couple of ways depending on your minimum API level. Starting from API 14 you may register ActivityLifecycle Callbacks inside an Android Application. To do this, i would recommend:
Inherit Android application with a custom one
Replace the Android application in your manifest
inside your custom application register itself as activity lifecycle listener
inside the abstract methods you get the instance of the currently applying activity (may safe object.name() in a String)
depending on your handling you may safe a boolean value or whatever to identify the behaviour
methods inside your custom application are accessible by casting (MyCustomApplication)getApplication()
Heres a snippet:
package com.example.preferencestest;
import android.app.Activity;
import android.app.Application;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;
public class MyCustomApplication extends Application implements ActivityLifecycleCallbacks {
private String storedActivity;
private Boolean doOrNot = false;
public MyCustomApplication() {}
public Boolean getDoOrNot() { return doOrNot;}
public void setDoOrNot(Boolean doOrNot) { this.doOrNot = doOrNot; }
#Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(this);
}
// these two are the most important ones since they will be fired everytime
#Override
public void onActivityResumed(Activity activity) {
if (activity.getClass().getName().equals(storedActivity)) {
doOrNot = true;
}
}
#Override
public void onActivityPaused(Activity activity) {
storedActivity = activity.getClass().getName();
}
#Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) { }
#Override
public void onActivityStarted(Activity activity) { }
#Override
public void onActivityStopped(Activity activity) { }
#Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) { }
#Override
public void onActivityDestroyed(Activity activity) { }
}
inside your Manifest you MUST declare this new Applicationclass like this:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.preferencestest"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="21" />
<application
android:name="com.example.preferencestest.MyCustomApplication"
{...}
Then inside your Activity you may do this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Boolean whatToDo = ((MyCustomApplication)getApplication()).getDoOrNot();
}
Using onConfigurationChanged has a couple of disadvantages. There are a couple of actions (placing device in dock, turning display and so on) which restart the Activity. You should rather save the current state of the Activity with
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
#Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
}