I'm finally looking into the new nested fragments APIs in the support library revision 11.
Everything It worked pretty well till I tried to use the activity reference held by the nested fragments.
After a configuration change the childFragment doesn't seem to get detached and re-attached to the new activity.
Basically after an orientation change my childFragment is in an inconsistent state from which I can't get the correct activity instance with getActivity().
I manged to get the correct one using getParentFragment().getActivity() and it works, but I don't think that's the right way to go.
here is the code I use to add the fragment in the parentFragment the first time, after that the fragment is automatically added back to the parentFragment:
public void addChildFragment() {
Fragment f = getFragment().getChildFragmentManager().findFragmentByTag( FRAGMENT_CHILD_TAG );
if (f == null) {
FragmentTransaction ft = getFragment().getChildFragmentManager().beginTransaction();
f = new TrackBrowserFragment();
f.setArguments( getFragment().getArguments() );
ft.add( R.id.fragment_album_detail_child_fragment_layout, f , FRAGMENT_CHILD_TAG );
ft.commit();
}
}
This inconsistent in the activity instance obviously lead to multiple problem with my fragment ( binds with services, broadcast receivers and so on ).
I'm probably doing something wrong cause I don't think that this is the correct behavior of a nested fragment.
so:
Am I doing something wrong with the code?
Is this the expected behavior of a nested fragment?
Am I missing something?
Should I detach/attach it by myself?
Thanks
I found out wich was the problem, i was using setRetainInstance(true) in the parent fragment and that kept the child fragment to be detached.
After I Removed that line everything works as expected
Related
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 have the following setup which is quite common: in landscape mode I have 2 fragments - A and B. In portrait mode I have only fragment A. I tried to detect if I am in second setup mode by just a simple check:
getSupportFragmentManager().findFragmentById(R.id.frag_b) == null
This was working fine until I was in 2 fragment mode and rotating the device to 1 fragment mode - after that the manager was finding the fragment B and not returning null. I believe the fragment manager was somehow saving and loading its state from previous setup. The first question - why is this working this way and what can I do with it?
Second question - I tried to remove the fragment but was not able to do that. Here how I tried:
Fragment f = manager.findFragmentById(R.id.frag_b);
manager.beginTransaction().remove(f).commit();
f = manager.findFragmentById(R.id.frag_b); // still there
I guess remove() didn't work since it was not added using add() but rather loaded from previous state xml. Is there a way to remove a fragment from manager in this case?
P.S. The solution for me will be to have another way of detection in which mode I am. I already have this, just need to know how it works for better understanding of fragments and their behavior.
you can detect if you are on portrait or landscape with this:
getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
Being new to fragments and after struggling a day I think I understood most of the logic behind fragments and their usage. The fact that fragment manager shows fragments other than the ones defined for current orientation is not a bug, it's a feature. Here are some observations summarized:
When changing configuration, the FragmentManager saves all the fragments it has currently and loads them back so they are ready in onCreate() method of container activity. This means that if you have fragment A and B in some layout and you rotate the device to a state where only A should be - you still will find B in FragmentManager. It might be not added (check with Fragment.isAdded()) or might be added to some other container, which is not visible now, but its there. This is quite useful since B saves its state (given that you did it properly in B's life cycle functions) and you don't have to take care of it on activity level. Maybe, at some point in future, you would like to dynamically add fragment B to your UI and it will have all its state saved from previous configuration.
Related to the second question above - fragments that need to be moved from container to container should not be declared in xml, they should be added dynamically. Otherwise you will not be able to change its container and will get IllegalStateException: Can't change container ID of fragment exception. Instead, you define containers in XML file and give them an ID, for example:
<RelativeLayout
android:id="#+id/fragmentContainer"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="0.7" />
and later add to it using something like
FragmentManager.beginTransaction().add(R.id.fragmentContainer, fragment);
If you need to use some fragment, first look if FragmentManager has it - if yes, then just reuse it - it will have its state saved as a bonus. To look for fragment:
private void MyFragment getMyFragment() {
List<Fragment> fragments = getSupportFragmentManager().getFragments();
if (fragments != null) {
for (Fragment f : fragments) {
// an example of search criteria
if (f instanceof MyFragment) {
return (MyFragment) f;
}
}
}
return null;
}
if it is null then create a new one, otherwise you can go ahead and reuse it, if needed.
One way of reusing a fragment is putting it in another container. You will need some effort in order not to get IllegalStateException: Can't change container ID of fragment. Here is the code I used with some comments to help understand it:
private void moveFragment(MyFragment frag) {
int targetContainer = R.id.myContainerLandscape;
// first check if it is added to a correct place
if (frag.isAdded()) {
View v = frag.getView();
if (v != null) {
int id = ((ViewGroup) v.getParent()).getId();
if (id == targetContainer) {
// already added to correct container, skip
return;
}
}
}
FragmentManager manager = getSupportFragmentManager();
// Remove the fragment from its previous container first (done
// here without check if added or nor, check if needed).
// In order not to get 'Can't change container ID...' exception
// we need to assure several things:
// 1. its not hardcoded in xml - you can remove()
// fragment only if you have add()-ed before
// 2. if this fragment is sitting deep in a backstack
// then you will still get the above mentioned exception.
// If the stack is fragA-fragB-fragC <-top then you get
// exception on moving fragment A. Need to clean the back
// stack first. HOWEVER, note that in that case you will
// lose the fragB and fragC together with their states!
// For that reason save them first - I will assume there is
// only one on top of current fragment to make code simpler.
// before cleaning save the top fragment so that it is not destroyed
OtherFragment temp = getOtherFragment(); // use function 'getMyFragment()' above
// clean the backstack
manager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
// remove the fragment from its current container
manager.beginTransaction().remove(frag).commit();
// call executePendingTransactions() for your changes to be available
// right after this call, otherwise the previous 'commit()' just submits
// the task to main thread and it will be done somewhere in the future
manager.executePendingTransactions();
if (getOtherFragment() == null && temp != null) {
// Add the fragment we wanted to 'save' back to manager, this
// time without any relation to backstack or container. Later
// we will be able to find it using getOtherFragment() and the
// fragment manager will be able to save/load its state for us.
manager.beginTransaction().add(temp, null).commit();
manager.executePendingTransactions();
}
// now add our fragment
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(targetContainer, frag);
transaction.commit();
manager.executePendingTransactions();
}
This was the result of my first day of dealing with fragments seriously, at least my task was accomplished in my app. Would be good to get some comments from experienced guys on what is wrong here and what can be improved.
First of all I'm sorry if this explanation seems unclear, I'm new to Android.
I have a ViewPager in main activity showing fragments added dynamically by user. Fragments are created initially on activity start up and are added to ViewPager via Adapter i.e. adapter simply returns proper fragment and as I understand correctly fragment's content is created at this time when ViewPager 'retrieves' a fragment first time.
The problem is when main activity gets restored after orientation changing all fragments are resurrected as well and when Adapter tries to return newly created by user Fragment method createView() is no longer called and it fails with NullPointerException. It seems ViewPager retains fragments attached to it initially and doesn't call createView() for newly added ones for the same position.
I have a feeling I'm missing vital point on the Fragment lifecycle. I wouldn't like to change the design. My main question is what the correct way is to return a Fragment added to ViewPage after activity is restored? Is there any way to locate recently attached fragments?
If the fragment already exists, it will be re-used. However the state of the fragment will not. You should take a look at http://developer.android.com/training/basics/activity-lifecycle/recreating.html for more information.
In particular you should look at overriding onSavedInstanceState and onRestoreInstanceState as a method to repopulate the field that is generating that nullpointerexception
To determine if an instance of a fragment has already been created you can use 'findFragmentByTag' like so:
String fragmentTag = MyFramgment.getClass().getName();
MyFragment frag = getFragmentManager().findFragmentByTag(fragmentTag);
if(frag == null)
frag = new MyFragment();
Whatever ends up being referenced in 'frag', show this to the user.
In my application, I use an Activity which holds one Fragment with FragmentTabHost and hence all its tabs are nested Fragments.
Inside an Activity which holds a Fragment with its nested Fragment, we may get a reference to attached one using onAttachedFragment().
But how to get a reference to nested Fragment from FragmentTabHost?
Well, exploring the source code of FragmentTabHost I've found that when it adds a fragment tab, it assignes a tag of TabSpec to nested Fragment.
So to get the reference to this Fragment we should call
getChildFragmentManager().findFragmentByTag(tabSpecTag)
I was trying this for a while, but I was getting null returned from the FragmentManager because I was trying to access the manager in onCreateView() immediately after adding.
Here is a good explanation on what happened
It's also important to note that Fragment tabs that have not yet been selected don't exist yet in the FragmentManager, and so will return null as well. I got around this by calling mTabHost.setCurrentTab(index) before trying get to the Fragment with the FragmentManager. It's not very clean, but it works.
Above solutions are also working but I have one more easy solution,
#Override
public void onTabChanged(final String tabId) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mFragment = getChildFragmentManager().findFragmentByTag("Tagname");
}
},1000);
}
Here you have to implement FragmentTabHost.onTabChangeListener
We have kept a second delay in fetching fragment from the childFragmentManager.
Note : You need to cast mFragment which fragment you have used.
I found a solution that I like a little better because it doesn't involving executing code with a delay (which is always iffy given android hardware fragmentation and different processor speeds).
In your onTabChanged() method, before you try to find the fragment, call executePendingTransactions() on the fragment manager associated with your tabHost. It seems there are some places in the FragmentTabHost source code where they should be calling executePendingTransactions() but fail to do so.
This works every time the tab changes with one exception... the first tab that is selected still comes back null... In my specific case, I was able to handle this exception differently anyway, by putting some code in onResume.
Hope this helps.
I am using Fragments to represents different views in my application. I replace the fragments using the following code when navigating between views:
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right);
ft.replace(R.id.main_linearlayout_fragmentcont, frag);
ft.addToBackStack(null);
ft.commit();
I have run into a number of problems when rotating and the activity is reconstructed. I need to support old versions of android so android:configChanges="orientation" isn't an option. A lot of the issues are due to the nature of how Android saves Fragment state.
These are the problems I am running into:
1) The Fragment transitions don't remember my custom animations for pop events when they are restored automatically after a rotate. They do however remember my BackStack. I know I can write my own back handler that does a replace using animations and get rid of pop all together but I was wondering if there is a way to either reset the animation before calling popBackStack() or a way to have the FragmentManager remember the animations when it auto restores after rotate.
2) The other issue I have is that I have a bunch of child views (linearlayouts) in one of my top level fragment views that contain their own fragments. These child views are created and populated programmatically. When my fragment is recreated after rotation, I programmatically reconstruct the child views in onCreateView of the Fragment Object and I end up with duplicate fragments under each of the child views (1 - I create programmatically and 1 - Android Fragments create from restore). I am assuming this is because I programmatically reconstruct the child views after rotation with the same id. Is there a way to prevent Fragments from being restored? When does Android inject the Fragments from savedState into these views I construct programmatically? How would I prevent this from happening?
3) The above replace code seems to fire onCreateView multiple times for my frag (Fragment) object. This is without rotation and happens when I run the above code only once. Is there a reason that onCreateView of a Fragment would be called multiple times with the above code?
Questions about Fragments:
1) Can I prevent Android from auto restoring fragments when an activity is reconstructed? How would I go about this? Is it based on the ID of the LinearLayout? Could I call removeAllViews of the LinearLayout containing the fragment onStop? That way the view doesn't exist when it saves?
2) Is there a way to add a Fragment to a LinearLayout that I have a reference to but that doesn't have an ID? It appears the Fragment add, replace APIs require an int ID.
Thanks!
1) if you find out how let me know, I'm also pissed off by that
2) you're probably calling add on the FragmentTransaction inside the top level fragment, but the restore operation is also adding, so duplicates! option 1. Use replace instead. option 2. (preferred) Check if(savedInstances==null) { // do transaction } else { //let the system rebuilt it itself}
3) If you're changing the layout (by calling add or replace) of a view that is a part of a fragment, the manager call the method to creates the view again. I'm still not sure if that is a bug or a feature, and if it's a feature why it is. If you find out let me know
1) (supposed to be 4, no?) don't mess with the layouts, if u want to remove, remove them using while(popBackStackImmediatly){}, but if you go deeper and understand what the system is doing, usually there's no reason to not let it do it automatically.
2) (supposed to be 5, no?) if you have a reference you have the id View.getId()
happy coding!
If you are change the orientation of device then check the validation in activity and it also manage the fragment with stack so your flow not damage in that case.
if(savedInstanceState == null) {
mFragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
mFragmentManager.beginTransaction();
FragmentOne fragment = new FragmentOne();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}