I'm working on my app since a few weeks. Now i pointed out the bug, that if i rotate my Device, it always replaces my Fragment to the "Home-Fragment".. and i dont know why. I Was searching it with "find in path"-function in Android Studio. I searched for keywords like:
-orientation
-setOrientation
-Landscape
-Portrait
But didnt find the code-snippet which changes my Fragment on rotation...
Any tipps? How can i find out where the "fragment replace" gets called?
the correct code to initialise the "home fragment" is like this:
public class MyActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.replace(android.R.id.content, new HomeFragment())
.commit();
}
}
}
note that you only do the fragment transaction if savedInstanceState == null, if it's not null, the Android and Fragment frameworks will automatically recreate everything.
I'm pretty sure the reason that you're always replacing with the homeFragment is because you're not checking savedInstanceState == null
Add android:configChanges="orientation|screenSize" to the manifest in your activity. This will prevent your activity to recreate view after orientation changes.
Related
I am using FragmentTransaction.replace() to swap fragments in and out.
The app starts up first time with no problem.
An IllegalStateException is thrown when rotating the device because of a conflict between the savedInstanceState
and commiting a new fragment transaction.
No AsyncTask is involved.
One StackOverflow question suggests to put the setContentView() call
in onResumeFragments(), but this seems to have no effect. Same with onPostResume().
Another StackOverflow question says to override onConfigurationChanged(). This works in that sense that it the exception doesn't occur
because the Activity is not restarted. However, this prevents fragments that have different portrait
and landscape layouts from switching between these layouts. Calling setContentView() in onConfigurationChanged()
causes a similar error (IllegalArgumentException: Binary XML file line #25: Duplicate id 0x12345678, tag null, or parent id with another fragment)
Using fragmentTransaction.commitAllowingStateLoss() instead of .commit() causes IllegalStateException: Activity has been destroyed.
How do I get this to work?
More exception info:
java.lang.RuntimeException: Unable to start
activity ComponentInfo{myapp/myap.MainActivity}:
android.view.InflateException: Binary XML file line #25: Error
inflating class fragment at
myapp.MainActivity.onResumeFragments(MainActivity.java:450)
Caused by: java.lang.IllegalStateException: Can not perform this action after
onSaveInstanceState at > android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1533)
at
myapp.fragments.FragmentChange.onFragmentChange(FragmentChange.java:128)
at
myapp.MainActivity.onNavigationDrawerItemSelected(MainActivity.java:490)
at
myapp.fragments.NavigationDrawerFragment.selectItem(NavigationDrawerFragment.java:197)
at
myapp.fragments.NavigationDrawerFragment.onCreate(NavigationDrawerFragment.java:78)
at myapp.MainActivity.onResumeFragments(MainActivity.java:450)
The sequence in the code upon rotating the device is:
MainActivity.onPause()
MainActivity.saveInstanceState()
NavigationDrawerFragment.onSaveInstanceState()
MainActivity.onStop()
MainActivity.onDestroy()
MainActivity.onCreate()
super.onCreate(savedInstanceState);
MainActivity.onResumeFragments()
setContentView()
NavigationDrawerFragment.onCreate()
MainActivity.onNavigationDrawerItemSelected()
fragmentTransaction.commit();
MainActivity:
public class MainActivity extends AppCompatActivity implements
NavigationDrawerFragment.NavigationDrawerCallbacks {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
}
#Override
public void onNavigationDrawerItemSelected(int position) {
...
FragmentChangeEvent fragmentChangeEvent = new FragmentChangeEvent(null);
FragmentChange fragmentChange = FragmentChange.getInstance( getSupportFragmentManager());
fragmentChange.onFragmentChange(fragmentChangeEvent);
...
}
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE");
super.onSaveInstanceState(outState);
}
#Override
public void onResumeFragments() {
super.onResumeFragments();
// causes onNavigationDrawerItemSelected() to be called, exception thrown
setContentView(myapp.R.layout.activity_main);
mNavigationDrawerFragment = (NavigationDrawerFragment)
getSupportFragmentManager().findFragmentById(myapp.R.id.navigation_drawer);
mNavigationDrawerFragment.setUp( // Set up the drawer
myapp.R.id.navigation_drawer,
(DrawerLayout) findViewById(myapp.R.id.drawer_layout));
}
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if current fragment is "Individual" { // pseudocode
setContentView(R.layout.activity_main); // causes IllegalArgumentException
}
}
}
NavigationDrawerFragment
public class NavigationDrawerFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);
if (savedInstanceState != null) {
mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
mFromSavedInstanceState = true;
}
// Select either the default item (0) or the last selected item.
selectItem(mCurrentSelectedPosition);
}
private void selectItem(int position) {
mCurrentSelectedPosition = position;
if (mDrawerListView != null) {
mDrawerListView.setItemChecked(position, true);
}
if (mDrawerLayout != null) {
mDrawerLayout.closeDrawer(mFragmentContainerView);
}
if (mCallbacks != null) {
// calls MainActivity.onNavigationDrawerItemSelected()
mCallbacks.onNavigationDrawerItemSelected(position);
}
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(STATE_SELECTED_POSITION, mCurrentSelectedPosition);
}
}
FragmentChange
public class FragmentChange implements FragmentChangeListener {
public static FragmentChange getInstance(FragmentManager fragmentManager) {
if (instance == null) {
instance = new FragmentChange(fragmentManager);
}
return instance;
}
// constructor
private FragmentChange(FragmentManager fragmentManager) {
mFragmentManager = fragmentManager;
}
#Override
public void onFragmentChange(FragmentChangeEvent fragmentChangeEvent) {
...
mPosition = fragmentChangeEvent.getPosition();
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
Fragment fragment = EmployeesVerticalFragment.newInstance();
fragmentTransaction.replace(myapp.R.id.container, fragment);
fragmentTransaction.commit(); // IllegalState exception here
...
}
}
A greatly reduced form of the project on github which reproduces the IllegalStateException:
Some comments:I can see you have gone "above and beyond" trying to fix it (and have scanned stackoverflow for many tips).
Your code works the first time (even if you are in landscape b4 starting app).
The rotation fails to inflate the fragment (second time around) (in activity_main), and also reports the "commit after save" error (that's weird,how on Earth are we getting 2 fatal errors ? maybe on another thread, or ""save" is killing the inflator, but how can it carry on?).
NavigationDrawerFragment has a LOT of important code in it (that normally goes in the Activity).
Analysis:
Your MainActivity class extends AppCompatActivity:
public class MainActivity extends AppCompatActivity implements
NavigationDrawerFragment.NavigationDrawerCallbacks {
Normally you extend FragmentActivity:
public class MainActivity extends FragmentActivity implements
NavigationDrawerFragment.NavigationDrawerCallbacks {
This is because you inflate activity_main, which contains a fragment.
setContentView(com.example.replacefragments.R.layout.activity_main);
Inheritance:
Activity <- FragmentActivity <- AppCompatActivity <- ActionBarActivity
'<-' means inheritance here.
One reason why you would need to consider FragmentActivity specifically is if you want to use nested fragments (a fragment holding another fragment), as that was not supported in native fragments until API Level 17.
I think this is what you are doing here (in activity_main):
<fragment android:id="#+id/navigation_drawer
Here is a good template app that should make your life easier:
android-sliding-menu-using-navigation-drawer/
FIXING IT:
Replace the fragment in activity_main.xml with the ListView.
I have discarded NavigationDrawerFragment, and injected some code from the template above, then re-introduced bit-by-bit your code from NavigationDrawerFragment, until it falls over.
It now works with rotation, but not as you or I would like (preserving fragment state, NavigationDrawerFragment some functionality), so I'm still working on it.
protraitlandscape after rotation
Hot tip:
The .commit sounds final does it not ? Sounds as if it should synchronous? Neither are true.
The .commit puts the transaction in a pending queue, to truly execute it you need:
getSupportFragmentManager().executePendingTransactions();
Here's a link to the error log of the posted code on rotation:
link error log
Method 1. Avoid config changes. Add android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard" in you manifest activity.
<activity
android:name=".ui.accounts.YouActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard"
android:theme="#style/AppTheme.NoActionBar" />
Method 2.
a. First of all do not miss place code from default places like setContentView() in onConfigurationChanged().
b. Replace fragment with tag or by id in xml
c. Don't replace again if fragment found by id or tag.
d. Make sure you are dismissing any dailog inside fragment onPause.
The FragmentManager is an
Interface for interacting with Fragment objects inside of an Activity.
It strikes me as a particular bad idea to have save it in a static field and reuse and old FragmentManager for a new activity. This will necessarily lead to Activity has been destroyed, when the new activity interact with the manager from the old activity.
In your code, replace
FragmentChange.getInstance(getFragmentManager());
by
new FragmentChange(getFragmentManager());
My code:
public class MainActivity extends AppCompatActivity {
private FragmentA fragmentA;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
fragmentA = FragmentA.newInstance();
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fragment_a_container, fragmentA, "FRAGMENT_A");
fragmentTransaction.commit();
}
else {
fragmentA = (FragmentA) getSupportFragmentManager().findFragmentByTag("FRAGMENT_A");
}
}
}
I don't really know what I am doing but this is currently what I do. I define a container for the Fragment and then I use a FragmentTransaction to replace it with a Fragment. The part I am confused about though is the else statement.
Should I be structuring this differently?
I thought configuration changes wiped out Activities and Fragments so why check for the Fragment in some support manager? Does this mean Fragments don't actually get destroyed? At the same time, they DO seem to get destroyed because they appear to reset unless I use onSaveInstanceState or the getArguments() approach.
Edit: What's wrong with doing this:
public class MainActivity extends AppCompatActivity {
private FragmentA fragmentA;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fragmentA = FragmentA.newInstance();
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fragment_a_container, fragmentA, "FRAGMENT_A");
fragmentTransaction.commit();
}
}
They do get destroyed and recreated for you on configuration changes by the, in this case, SupportFragmentManager.
To answer your questions:
Should I be structuring this differently?
No, that's exactly how you should create fragments if there is no saved state and retrieve them when there is. See also my answer here;
a) so why check for the Fragment in some support manager?
Because the manager handles the lifecyle of the fragment for you when there is a configuration change.
b) Does this mean Fragments don't actually get destroyed?
No, it does get destroyed. See this diagram for a reference.
Edit to answer some of your questions from the comments:
But any member variables inside that Fragment are completely lost on configuration change unless I save them in that Fragment's onSaveInstanceState, right?
That is correct. Because your fragment is being destroyed, everything not being saved on onSaveInstanceState gets lost.
So then what exactly am I restoring?
You are not restoring anything. You are only retrieving the reference to the fragment that was previously created. You restore your variables on the onRestoreInstanceState() method of your fragment.
What's wrong with doing this (the code from the edit in the question)?
If you do that, you are adding a new fragment instance to the R.id.fragment_a_container container. So the old fragment will get lost together with the state of it you saved on onSaveInstanceState(). It will be a new fragment, with new information in it and the event onRestoreInstanceState() won't be called for it.
I'm facing a strange problem inside my app. From a fragment, if i push a button, i start new FragmentActivity that contains a fragment and some other elements, but if i would go back to previous Activity (that contain fragment that start the current activity), i need to push back button twice.
First time fragmentActivity seem close itself, but it reopens again. I close FragmentActivity as always:
#Override
public void onBackPressed() {
super.onBackPressed();
finish();
}
so what's wrong?
i start new FragmentActivity that contains a fragment and some other elements
You're most likely adding that FragmentTransaction to the backstack, which is why it's consuming the back button press. You shouldn't need to override onBackPressed() at all. Instead, look at the code where you add the Fragment to the new FragmentActivity and ensure you're not calling .addToBackStack() on the initial transaction. For example, typically in onCreate() you would do something like this:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.your_container_id, new YourInitialFragment())
.commit();
}
}
Note I've checked the very numerous "duplicates" of this question and none of them fit the bill, so please don't mark this as a duplicate.
I modified the default wizard-created app (in Android Studio) to try to find the placeholder fragment after it is created, like this (the only statement added is the Log line):
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, new PlaceholderFragment())
.commit();
}
Log.d("", "Found fragment: " + getSupportFragmentManager().findFragmentById(R.id.container));
}
However the log just prints:
Found fragment: null
According to what I've read of the documentation and answers to similar questions, it should work. What's going on?
The fragment transaction has not yet been executed but just scheduled for later execution.
Wait for super.onStart() in the application lifecycle, of if you're impatient, call executePendingTransactions().
My activity invokes the camera with the ACTION_IMAGE_CAPTURE intent. If the camera activity returns succesfully, I set a flag in the onActivityResult callback, and based on the value of the flag I start a fragment in my onResume to add a caption to the image that was captured. This seems to work ok.
I just got a stack trace from the "wild" complaining that I was trying to commit a fragment transaction after onSaveInstanceState has been called. But I'm doing the commit in my onResume method! Why would android complain about this? I do have android:configChanges="orientation|keyboardHidden|keyboard|screenSize" set in my AndroidManifest.xml, so an orientation change should not trigger this....
This occurred on a Samsung Galaxy S3 (SGH-i747) running 4.0.4
Here is the stack:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1314)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1325)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:548)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:532)
at com.Familiar.Android.FamiliarAppV1.AddPhotosActivity2.performFragmentTransition(AddPhotosActivity2.java:278)
at com.Familiar.Android.FamiliarAppV1.AddPhotosActivity2.switchToCaptionsFragment(AddPhotosActivity2.java:438)
at com.Familiar.Android.FamiliarAppV1.AddPhotosActivity2.onResume(AddPhotosActivity2.java:167)
at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1158)
at android.app.Activity.performResume(Activity.java:4544)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2448)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2486)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1187)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4514)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:980)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:747)
at dalvik.system.NativeStart.main(Native Method)
Any help or wisdom is appreciated.
I think I know the answer - I'm using the FragmentActivity from v4 compatibility library, and so I need to perform my fragment transactions in onResumeFragments instead of in onResume. Can someone confirm?
you can use the method commitAllowingStateLoss()
but be aware you can lose the state of your activity
as you can see in google's android reference
which explain the different between the two in the following way
Like commit() but allows the commit to be executed after an activity's state is saved. This is dangerous because the commit can be lost if the activity needs to later be restored from its state, so this should only be used for cases where it is okay for the UI state to change unexpectedly on the user.
from my experience it may cause the addToBackStack method not to work sometimes so you will need to add it manually on the fragment
and of course the state won't be saved (textbox text ext.)
this worked for me... found this out on my own... hope it helps you!
1) do NOT have a global "static" FragmentManager / FragmentTransaction.
2) onCreate, ALWAYS initialize the FragmentManager again!
sample below :-
public abstract class FragmentController extends AnotherActivity{
protected FragmentManager fragmentManager;
protected FragmentTransaction fragmentTransaction;
protected Bundle mSavedInstanceState;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSavedInstanceState = savedInstanceState;
setDefaultFragments();
}
protected void setDefaultFragments() {
fragmentManager = getSupportFragmentManager();
//check if on orientation change.. do not re-add fragments!
if(mSavedInstanceState == null) {
//instantiate the fragment manager
fragmentTransaction = fragmentManager.beginTransaction();
//the navigation fragments
NavigationFragment navFrag = new NavigationFragment();
ToolbarFragment toolFrag = new ToolbarFragment();
fragmentTransaction.add(R.id.NavLayout, navFrag, "NavFrag");
fragmentTransaction.add(R.id.ToolbarLayout, toolFrag, "ToolFrag");
fragmentTransaction.commitAllowingStateLoss();
//add own fragment to the nav (abstract method)
setOwnFragment();
}
}
Update I think I have found an explanation and a solution here: http://code.google.com/p/android/issues/detail?id=23096#c4
I implemented the Empty Fragment Workaround posted there and got no more IllegalStateException so far.
I add the invisible state fragment in my activity like this;
#Override
protected void onCreate(final Bundle args) {
...
if (args == null) {
final FragmentManager fm = this.getSupportFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
final Fragment emptyFragmentWithCallback = new EmptyFragmentWithCallbackOnResume();
ft.add(emptyFragmentWithCallback, EmptyFragmentWithCallbackOnResume.TAG);
ft.commit();
}
The following code is taken from above link:
public class EmptyFragmentWithCallbackOnResume extends Fragment {
OnFragmentAttachedListener mListener = null;
#Override
public void onAttach(SupportActivity activity) {
super.onAttach(activity);
try {
mListener = (OnFragmentAttachedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnFragmentAttachedListener");
}
}
#Override
public void onResume() {
super.onResume();
if (mListener != null) {
mListener.OnFragmentAttached();
}
}
public interface OnFragmentAttachedListener {
public void OnFragmentAttached();
}
}
and invoke fragment transactions that would intuitively go into onResume or onResumeFragments into my custom onFragmentAttached method that is invoked by the invisible state fragment. I do not use onResumeFragments at all and do not issue any fragment transactions in onResume.
So, to sum it up. If you are using the support lib and fragments, pretty much forget about onResume, forget about onResumeFragments and implement your own "onResume" based on the above workaround.
This is somewhat ridiculous.
I cannot confirm. I have the exact same problem. even though I am issuing fragment transaction in onResumeFragments.
This used to work as I posted here: IllegalStateException - Fragment support library .
It seems the error only occurs on 4.0.3 and 4.0.4. However it does neither occur always nor in my Emulator.
I am using support lib rev. 10 and API 16.
I call DialogFragment.show in onResumeFragments and continously get this ridiculous exception from some random users. I cannot reproduce it locally.
I was always getting this when I tried to show fragment in onActivityForResult() method, so the problem was next:
My Activity is paused and stopped, which means, that onSaveInstanceState() was called already (for both pre-Honeycomb and post-Honeycomb devices).
In case of any result I made transaction to show/hide fragment, which causes this IllegalStateException.
What I made is next:
Added value for determining if action I want was done (e.g. taking photo from camere - isPhotoTaken) - it can be boolean or integer value depending how much different transactions you need.
In overriden onResumeFragments() method I checked for my value and after made fragment transactions I needed. In this case commit() was not done after onSaveInstanceState, as state was returned in onResumeFragments() method.
I first develop my app targeting android 2.2 (SDK 8), using the support v4 library, and when I start using it with 4.2 (SDK 17), I had the same troubles with my fragments. But changing my Manifest to android:minSdkVersion="8" android:targetSdkVersion="17", solved my problems. Maybe this hepls you too.
Add this to your Activity:
#Override
protected void onSaveInstanceState(Bundle outState) {
//No call for super(). Bug on API Level > 11.
}