I have a FragmentStatePagerAdapter that shows 3 tabs and of which, the 1st tab (fragment) commits a FragmentTransaction inside it's onCreateView(). For some users, I am seeing an IllegalStateException which is caused by commit() state loss.
After reading up on a few stack overflow Q&As and a blog post by Alex Lockwood on Fragment Transactions and Activity State Loss, I better understand what is causing the state loss and I can take several approaches.
I can move my fragment commit to onPostResume.
I can move my fragment commit to onCreate() - probably safest.
However, there is a lot of logic inside onCreateView() and if I were to take the above approach, I may introduce regression bugs. I am wondering, since onCreate() will eventually call onCreateView(), if it is okay to just check savedInstanceState for null. Would this solve the problem?
Here is my current code:
#Override
public View onCreateView(#NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
...
FFragment frag = new FFragment();
frag.setDateSelected(mDateSelected);
getFragmentManager().beginTransaction()
.add(R.id.container, frag, FRAG_TAG)
.commit();
...
}
What I imagine would fix the state loss:
#Override
public View onCreateView(#NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
...
if(savedInstanceState == null) {
frag = new FFragment();
frag.setDateSelected(mDateSelected);
getFragmentManager().beginTransaction()
.add(R.id.container, frag, FRAG_TAG)
.commit();
} else {
frag = (FFragment) getSupportFragmentManager()
.findFragmentByTag(FRAG_TAG);
}
...
}
Why am I asking this and not just implementing and testing it? I've tried to kill the activity through ADB and DDMS but was not able to reproduce the state loss - thus asking if this approach would work. Or, do I have to do the inevitable and move the commit to onCreate or onPostResume()?
Is there another way to test trigger state loss?
There are a couple of things you got wrong
You are using getFragmentManager() and getSupportFragmentManager() inside a fragment, you should alwasy use getChildFragmentManager() when using nested fragments
OnResumeFragments() and onCreate that Alex talked about are in respect to activity and not fragments, there is not such method inside a framgent
The solution you are proposing is for an entirely different thing, it is used when the activity is recreated from the backstack
The solution would be to move your code to onViewCreated() method of your fragment and use childFragmentManager, I am using a viewpager inside a fragment and I initialize it inside onViewCreated(), haven't faced any such problem till now
Related
I am using nested fragments in the bottom navigation. at the first time, all fragments were loaded and everything is fine but when I changed the fragment, null exception fired and I saw that getParentFragment() return null since onDetach get called I need to know that in child fragment what happens in that func? child fragment gets detach from its parent and I have to set it again? how to avoid this detaching?
public void switchFrag() {
Fragment displayedFragment;
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
if (isListFragmentDisplayed) {
displayedFragment = mapFragment;
transaction.replace(R.id.fragment_contaner, displayedFragment, "map").commit();
} else {
displayedFragment = listFragment;
transaction.replace(R.id.fragment_contaner, displayedFragment, "list").commit();
}
isListFragmentDisplayed = !isListFragmentDisplayed;
}
The detach method removes the fragment from the UI, but its state is maintained by the Fragment Manager. This means you can reuse this fragment by calling the attach method, with a modified ViewHierarchy
Nested fragments are supported as of Android 4.2.
you can use getChildFragmentManager() ...
Detach:
Detach the given fragment from the UI. This is the same state as when
it is put on the back stack: the fragment is removed from the UI,
however its state is still being actively managed by the fragment
manager. When going into this state its view hierarchy is destroyed.
I have a thread which triggers a callback method in my Activity which has a fragment. The Activity then calls a method within the Fragment to refresh the UI with new info. In that method, getString() is called. When getstring() is called, I get a 'Fragment not Attached to Activity error'. This UI refreshing error is guarded runOnUIThread. The fragment is added like so:
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction()
.add(fragmentContainerID, fragment, tag)
.commit();
fm.executePendingTransactions();
The activity uses two different layout files for horizontal and vertical orientation. Their are two fragment containers in both layouts. They have the same names in both horizontal and vertical layouts, but they do not match each other. The fragments are added using the above code the first time the Activity is created. On orientation changes, the fragments are automatically added into the layouts because the fragment containers have the same ids in both vertical and horizontal orientation.
When the savedInstanceState is not null, I get a reference to the fragments by using
fm.findFragmentByTag
Can anyone please tell me what I am doing wrong ? The fragments are visibly added to the Activity perfectly fine, and several minutes go by before this thread callback occurs, so I don't see how the Fragment is not "attached".
EDIT: Sigh, after some debugging Ive found that adding fragments in this way is perfectly fine. Code elsewhere in the Activity ( I registered for a callback twice) caused my fragment not attached error. If anyone reading this is getting this error, keep in mind that this issue can be triggered from elsewhere. Also, you have to setRetainInstance to true in the fragment to avoid it being recreated. Tags alone don't do that.
Fragments are added one-per-container so if you are using .add(fragmentContainerID, fragment, tag) ensure firstly that fragmentContainerID is different for each of the fragments added.
However, these sorts of issue are most often caused by a misunderstanding in how orientation changes are handled. When you change orientation and let the oncreate handle the adding: you get a new fragment of the same type what this means is that if your background task has a reference to your fragment - it has a reference to the old (now detached) fragment. What you need to do is to make sure the same fragment gets added to the tag (not a new instance). To do that just don't (re)create a fragment instance when we are only restoring state
public class MyActivity ... {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getSupportFragmentManager();
if (savedInstanceState == null) {
MyFragment frag = new MyFragment();
frag.setArguments( getIntent().getExtras() ); //if it takes params
fm.beginTransaction().add(fragmentContainerID, frag, tag).commit();
}
//after this point a call to MyFragment frag = (MyFragment) fm.findFragmentByTag(tag); should always return the correct fragment
}
It's probably also a wise idea for your task to look for the fragment right when it does its callback to update the UI (and NOT to hold on to a reference to it). Depending on how you've set up your task it just means changing from FragmentManager fm = getSupportFragmentManager(); then the task is created, to calling it as part of the onPostExecute
When change orientation, fragment automatically added to FragmentManager and forget own tag!!
Use this code to find your fragment:
FragmentManager fragmentManager= getSupportFragmentManager();
for (Fragment fragment : fragmentManager.getFragments())
{
if(fragment instanceof MyFragment)
myFragment = (MyFragment)fragment;
}
After you find the fragments on orientation change, try detaching them and then reattaching them before any operation. Or if that does not work remove them and add them. then continue..... I have even had to sometimes put a time delay of a few millisec after detach and before attach to ensure all OK.
I am rewriting a Bluetooth app with 3 Activities to use just 1 Activity and 3 Fragments:
So I have now 4 files:
MainActivity.java (contains bluetooth and shared preferences code)
MainFragment.java (contains ellipsis menu to show SettingsFragment)
SettingsFragment.java (contains "scan" button to show ScanningFragment)
ScanningFragment.java (displays nearby bluetooth devices in a list)
It almost works, but as an Android programming newbie I don't understand - what to do with Fragments when I show some other Fragment?
Should I just drop the Fragments (and remove from FragmentManager?) to be garbage collected?
Or should I add these 3 private variables to MainActivity.java and reuse them (when the user navigates forwards and backwards)?
private MainFragment mMainFragment;
private SettingsFragment mSettingsFragment;
private ScanningFragment mScanningFragment;
Or does FragmentManager somehow manage all 3 Fragment for me - regardless if they are visible or not?
Here is my current code (it is simple, I just call replace() all the time)-
public class MainActivity extends Activity implements
MainListener,
SettingsListener,
ScanningListener,
BleWrapperUiCallbacks {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_main); // empty FrameLayout
Fragment fragment = new MainFragment();
getFragmentManager().beginTransaction()
.replace(R.id.root, fragment, "main")
.commit();
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
Fragment fragment = new SettingsFragment();
getFragmentManager().beginTransaction()
.addToBackStack(null)
.replace(R.id.root, fragment, "settings")
.commit();
break;
}
return super.onOptionsItemSelected(item);
}
// implementing SettingsFragment.SettingsListener interface
public void scanClicked() {
// TODO how to stop indicator when returned?
setProgressBarIndeterminateVisibility(true);
String address = // get from shared preferences
Fragment fragment = ScanningFragment.newInstance(address);
getFragmentManager().beginTransaction()
.addToBackStack(null)
.replace(R.id.root, fragment, "scan")
.commit();
}
Should I just drop the Fragments (and remove from FragmentManager?) to
be garbage collected?
No need to do anything else. FragmentManager is the guy in charge of Fragments' lifecycle. Once you call replace(), FragmentManager takes care for the rest. If needed it will keep fragment in memory, or release it.
Or should I add these 3 private variables to MainActivity.java and
reuse them (when the user navigates forwards and backwards)?
No, don't do it because of the said above.
Or does FragmentManager somehow manage all 3 Fragment for me -
regardless if they are visible or not?
Yes, it does. For instance, if you have invisible retained fragment, it's enough to create it once, and FragmentManager will take care of it and will keep it even when activity gets re-created during configuration change.
If you create fragments dynamically (as far as I can see, this is your case) then I suggest to add very first fragment dynamically too. You can do it like this.
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_main); // empty FrameLayout
if (savedInstanceState == null) { // <- important
Fragment fragment = new MainFragment();
getFragmentManager().beginTransaction()
.replace(R.id.root, fragment, "main")
.commit();
}
}
This will ensure you don't duplicate MainFragment on configuration change, because when savedInstanceState is not null, then FragmentManager keeps instance of your fragment already.
Since you are calling .replace() on the fragment manager it's essentially the same thing as calling .remove(). According to the docs:
This is essentially the same as calling remove(Fragment) for all currently
added fragments that were added with the same containerViewId and
then add(int, Fragment, String) with the same arguments given here.
So you don't need to worry about any further management since it will be taken care of for you (and be removed to free up resources). This basically means that when one is shown the other is removed. If you were to call .add() then the fragments would still be alive in the background using up resources but you don't have to worry about that since using .replace() only allows one to live at a time.
If I understand your question correctly, you need not call any method to destroy the fragments after using them. Android OS will take of them. According to the documentation, When you replace the fragment with another, the onStop() method of the fragment will be executed, and documentaed as,
The fragment is not visible. Either the host activity has been stopped or the fragment has been removed from the activity but added to the back stack. A stopped fragment is still alive (all state and member information is retained by the system). However, it is no longer visible to the user and will be killed if the activity is killed.
So the fragment will be killed by the OS when the activity is killed. Till the activity is live, fragment objects will reside in the memory.
EDT:
So, if you want to use the fragment again in future, as document suggests,
Also like an activity, you can retain the state of a fragment using a Bundle, in case the activity's process is killed and you need to restore the fragment state when the activity is recreated. You can save the state during the fragment's onSaveInstanceState() callback and restore it during either onCreate(), onCreateView(), or onActivityCreated(). For more information about saving state, see the Activities document.
Fragment are hard coded in xml can not replaced....
but when you call replace(..) then what happen ??
Ok just consider you have 3 fragment A,B,C . In primary stage A initialize in MainActivity... now you are going to call B from A using replace(....). that means A will go onPause(),onStop() state of lifecycle and B will be initialized and paced in MainActivity...same as for C then B is onPause() ,onStop() state. If you want to reuse A or B then you need to call again A or B from C using replace (..). and then A or B will reinitialize and C goes to onPause(),onStop(). But there is another way you can do this
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
hide(A);
show(B);
ft.commit();
if you use above code block then A still be in the running state of its lifecycle but not visible (just detached from UI ).. so it would be better if you need those fragment again use hide()or show() method because it is less expensive operation then reinitialize the fragment.
My older version of an Android app was using 4 different activities (FirstActivity => FourthActivity, with the corresponding xml activity_first => activity_fourth), and the app can switch back and forth between those using Intent. Recently I wanted to change the user interface to use a ViewPagerIndicator. I have implemented 4 fragments like this:
public class FirstFragment extends Fragment{
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
return inflater.inflate(R.layout.activity_first, null);
}
}
The question is, how can I migrate all the business code from FirstActivity to FirstFragment? Do I just need to find the equivalents of onCreate, onDestroy, onStart... and copy/paste the code (adding getActivity(), getView() where appropriate)? Is there any easy way to attach the fragment to an activity to avoid doing so?
The easiest way to do it is to migrate the code you had in your individual activities under onCreate() to onActivityCreated() in the new fragments. You then add the fragments to your activity using a fragment transaction in the onCreate() method of your supporting activity. The android docs give a pretty good walk through of how to do this here, complete with sample code. In case there's some confusion you're going to do the following in your underlying activity:
Get a new FragmentManager
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Create a new instance of your fragment
ExampleFragment fragment = new ExampleFragment();
Add that fragment to your FragmentManager
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
There are a number of different ways to use fragments - like in a ViewPager or ViewSwitcher that require a different implemenation but this sounds like it address what you're trying to do.
I am trying to understand a bad behaviour in fragments: the onCreateView and onActivityCreated methods are called even the fragment is not 'visible' in the layout.
If you use the code:
TestFragment testFragment = new TestFragment();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.fragmentDetail, testFragment, "test");
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
replacing the FrameLayout with id fragmentDetail with the fragment and then you rotate the device, the fragments method are still invoked even if the container is not present anymore in the portrait layout. This doesn't happen if you use the 'static' <fragment> tag.
If you use the static fragment, the fragments methods are invoked just when the fragment appears. Is it possible to achieve the same behaviour without using the fragment tag? I need a way to avoid the rendering of the fragment if it is not in the layout.
Thanks
I have found one fix to this. It is slightly different from the suggested Handling orientation changes with Fragments one:
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (!fragment.isInLayout() && container == null) return null;
...
}
In this way you can avoid the case when the fragment is statically put into the layout (in that case the container is null but the method isInLayout() returns true.
By the way it is still weird to me this behaviour.
AFAIK, fragments work almost as Activities. They have the same lifecycle. http://developer.android.com/reference/android/app/Fragment.html#Lifecycle So, if you don't have references to them, it won't make them close. They are referenced by the system and live by themselves. So, you should finish them somehow.