I am confused by Fragment managers and fragments. In the book "Android Programming" by big nerd ranch, they instantiate a fragment in the MainActivity.
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragmentContainer);
if (fragment == null) {
fragment = new CrimeFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit();
}
In the second line, they find the fragment not by the ID of the fragment, but by the ID of the fragmentContainer. FragmentContainer is just a plain layout. The book says that "[the container view ID] is used as a unique identifier for a fragment in the FragmentManager's list.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
On a different page, when coding this fragmentContainer, they write "You can and will use this same layout to host other fragments."
If this layout can host other fragments, how can this layout be used as a unique identifier for a fragment in the FragmentManager's list? Why don't they do
fm.findFragmentById(R.id.[some fragment id here])
and instead using the Id of a fragment container that that can host other fragments as well?
they find the fragment not by the ID of the fragment, but by the ID of
the fragmentContainer.
The cool thing is that the Fragment ID and the Fragment Container ID are the same thing! Update your code to add some logging statements:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crime);
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
if (fragment != null) {
Log.d(TAG, "fragment id: " + fragment.getId());
}
View fragmentContainer = findViewById(R.id.fragment_container);
Log.d(TAG, "fragment container id: " + fragmentContainer.getId());
if (fragment == null) {
fragment = new CrimeFragment();
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
}
Then run the app and rotate the device:
CrimeActivity: fragment container id: 2131296335
DEVICE ROTATION
CrimeActivity: fragment id: 2131296335
CrimeActivity: fragment container id: 2131296335
Obviously, your ID will be different than mine.
When you create the FragmentTransaction
fm.beginTransaction().add(R.id.fragmentContainer, fragment)
the FragmentManager will use the ID as both a way to identify this fragment from the list of fragments that it is managing and as the location in the view hierarchy to display the fragment.
On a different page, when coding this fragmentContainer, they write
"You can and will use this same layout to host other fragments."
If this layout can host other fragments, how can this layout be used
as a unique identifier for a fragment in the FragmentManager's list?
That last sentence should be clarified to read
"You can and will use this same layout to host other fragments, by replacing the current fragment with a different one."
There will only ever be one fragment with that ID and only one spot in the view hierarchy with that id. Jump ahead to code listing 17.8 (in the second edition) to see a replacement example.
if (findViewById(R.id.detail_fragment_container) == null) {
Intent intent = CrimePagerActivity.newIntent(this, crime.getId());
startActivity(intent);
} else {
Fragment newDetail = CrimeFragment.newInstance(crime.getId());
getSupportFragmentManager().beginTransaction()
.replace(R.id.detail_fragment_container, newDetail)
.commit();
}
The replace call with throw way whatever Fragment was currently in detail_fragment_container and insert newDetail fragment, setting it's ID to R.id.detail_fragment_container.
Why not use Tags?
One comment suggested:
I would use tags: .add(R.id.fragmentContainer, fragment, "MY TAG") and
then: findFragmentById("MY TAG")
This doesn't directly solve the problem. Now you have two ways to identify a Fragment: it's Tag and it's ID (a Fragment will still be given an ID just by the fact that you are adding it to R.id.fragment_container).
Given two ways to identify the Fragment, it will be up to you as a developer to keep track of what is displayed where. Which Fragment is this (what is it's TAG) and where is it being displayed (what is it's ID)?
Finds a fragment that was identified by the given id either when
inflated from XML or as the container ID when added in a transaction.
This first searches through fragments that are currently added to the
manager's activity; if no such fragment is found, then all fragments
currently on the back stack associated with this ID are searched.
That's the docs for findFragmentById( ). If the fragment was inflated from XML using the fragment tag, then it will do exactly as you said. If it was added dynamically by using a transaction, it'll look for fragments with the associated container ID.
Related
I am working on an app that has the following UI structure:
One Activity, called FoodActivity
This Activity implements bottom navigation. Hence, can show three categories of food: fruit, meat and fish
The Activity has a big Fragment container, where I attach the Fragment for fruit, meat and fish as the user interacts with the bottom tabs.
Each outer Fragment (fish, meat and fish) presents navigation between Fragments: it can show a list of fruits, and the the detail for the selected fruit.
Hence, the outer Fragments have another Fragment container in where I attach Fragments for the fruit list or fruit detail.
So, it's one main Activity, which has a big Fragment container where I swap Fragments, which in turn nest other Fragments
To switch from one outer Fragment to another using the tabs (ie: switch from fruit to meat), I perform a Fragment Transaction in the outer Fragment container:
private void switchFragment(Fragment fragment, String fragmentTag) {
final FragmentManager fm = getSupportFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragment_outer, fragment, fragmentTag);
ft.commit();
}
The problem is that when switching the first-level Fragments, the state of their ChildFragmentManager is not kept.
For example, imagine that:
- I start in FuitFragment
- The FruitFragment, when created, attaches the nested FruitListFragment
- The user navigates in the nested Fragment container, from FruitListFragment to FruitDetailFragment
- The user switches to another tab
- The user switches back to the fruit tab
- The child `FragmentManager* of the FuitFragment does not automatically put FruitDetailFragment in the nested fragment container.
More specifically, when switching back to the outer Fragment:
onCreate is called, with savedInstance == null
onCreateView is called, with savedInstance == null
Hence, as the outer Fragment onCreate method is called, and I cannot tell whether or not to attach the child Fragment (as one usually does with Activities).
Moreover if I don't attach it (using an instance variable to check if I'm switching back to it) the child Fragment container will be just empty.
Experiments and notes
I have experimented that if I add the outer fragment transaction to the backstack when attaching it, when switching back to it, onCreate method is NOT called and the child FragmentManager will remember and attach the fragment it had before.
However, I cannot add it to the backstack as per Google's specifications on bottom navigation.
Setting setRetainInstace to true does not make any effect.
So, what should I do for properly restoring the state of the child FragmentManager?
Am I doing something wrong, or is it that everything around nested Fragments in Android is not well supported (or a bit broken) and I simply should not provide navigation using nested Fragments?
As Abbas pointed out, the problem was that I was using replace to switch between fragments.
I have changed to code in the Activity that puts the outer Fragment, and it works:
private void showChildFragment(int itemId) {
final FragmentManager fragmentManager = getSupportFragmentManager();
final FragmentTransaction transaction = fragmentManager.beginTransaction();
final Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.fragment_outer);
if (currentFragment != null) {
Log.v(TAG, "Detaching item #" + currentFragment);
currentFragment.setMenuVisibility(false);
currentFragment.setUserVisibleHint(false);
transaction.detach(currentFragment);
}
// Do we already have this fragment?
final String tag = makeFragmentTag(container.getId(), itemId);
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment == null) {
fragment = createFragmentForViewId(itemId);
Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
transaction.add(container.getId(), fragment, tag);
} else {
Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
transaction.attach(fragment);
}
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
transaction.commitAllowingStateLoss();
fragmentManager.executePendingTransactions();
}
private Fragment createFragmentForViewId(int itemId) {
switch (itemId) {
case FRAGMENT_ID_LIBRARY:
return LibraryNavigationFragment.createInstance();
case FRAGMENT_ID_FEED:
return WebAppFragment.createInstance("feed");
case FRAGMENT_ID_SUGGEST:
return WebAppFragment.createInstance("suggest");
default:
throw new IllegalArgumentException();
}
}
This code is almost copy pasted from android.support.v4.app.FragmentPagerAdapter as ViewPagers using Fragments work like I wanted to.
With getChildFragmentManager() it won't crash.
private void switchFragment(Fragment fragment, String fragmentTag) {
final FragmentManager fm = getChildFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragment_outer, fragment, fragmentTag);
ft.commit();
}
I am using navigation drawer and it is simple to use. I am not providing the complete code but providing you detail which could be easy for you to understand my problem. I am using fragments these are about 8 in numbers and I am replacing them with one an other. But here comes a problem
I am replacing them on click event of the navigation drawer. but there are two main problems
After replacement , I can see the previous fragment in the background. does replace method just call the new fragment over it ? if yes then what should I do to old fragment not be visible in the background of my new fragment.
When I click navigation drawer Item , it loads the specific fragment successfully. but keeping in that fragment when I click to that specific item again it loads this fragment again and again. For example if drawer item num 3 opens fragment MyBook , then by clicking item num three 2 or many times would open fragment that much time.
So please some one answer me how to cure my app for such kind of actions which I described above.
I tried like this. Its working fine me
FragmentManager frgmanager = getFragmentManager();
frgmanager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
FragmentTransaction frgTransaction = frgmanager.beginTransaction();
if(subitem.equalsIgnoreCase("subitem")){
Frag1 frg1 =new Frag1(mCtx);
frgTransaction.replace(R.id.inflate_layout, frg1);
}else if(subitem1.equalsIgnoreCase("subitem1")){
Frag2 frg2 =new Frag2(mCtx);
frgTransaction.replace(R.id.inflate_layout, frg2);
}else{
Frag2 frg3 =new Frag3(mCtx);
frgTransaction.replace(R.id.inflate_layout, frg3);
}
frgTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
frgTransaction.commit();
you can use addtobackstack in fragmentstranstion object.like
FragmentManager manager = getFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.bodyfragment, new AnotherFragment());
transaction.addtoBackStack(null).commit();
Use replace-method of FragmentTransaction instead of add (http://developer.android.com/guide/components/fragments.html#Transactions)
FragmentManager manager = getFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.bodyfragment, new AnotherFragment());
transaction.commit();
To avoid re-instantiating the fragment, keep track of the current open fragment and only do a fragment transaction, if we next-to-be-opened fragment is a different one than the current.
This may achieved like the following:
class MyActivity ... {
private String currentFragment;
private void openNewFragment(Fragment fragment) {
String newFragment = fragment.getClass().getSimpleName();
if (newFragment.equals(currentFragment)){
// new fragment already shown
return;
}
// Fragment transaction etc here:
}
}
Note that this only compares fragments based in their class name. Sometimes this might not be unique, e.g. if there is a DetailFragment class which displays information about an entity. Which entities details to show may depend on intent arguments.
The above code however will then prevent opening DetailFragment for Entity=1 if currently details for Entity=2 are shown. For these scenarios the information about the fragment kept needs to be extended (e.g. storing a Reference or WeakReference to the fragment instance itself).
I am building a navigation drawer as designed by the google documentation however I have an issue where the fragment is not being replaced. http://developer.android.com/training/implementing-navigation/nav-drawer.html
When the app first loads, the default fragment is loaded.
Clicking on another item on the drawer list leaves an empty view
However on rotating the device, loads the fragment chosen.
public void selectNavActivty(int position){
// TODO Changing between the different screens selection
fragment = null;
switch (position) {
case 0:
fragment = OverLay.newInstance();
break;
case 1:
fragment = Dummy.newInstance();
break;
}
if(fragment != null) {
// attach added to handle viewpager fragments
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.content_frame, fragment).attach(fragment)
.addToBackStack(null);
trans.commit();
getFragmentManager().executePendingTransactions();
} else {
Log.d("Drawer Activity","Error in creating Fragment");
}
}
For navigation menu fragment transactions I use the following approach, this way the fragment will be added and placed on top.
String name = "myFragment";
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.content_frame, fragment, name)
.commit();
Look up the attach() function. It follows a different fragment lifecycle.
Also make sure that your layout files framelayout is visible.
Modify your code as below:
if(fragment != null) {
// attach added to handle viewpager fragments
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.content_frame, fragment);
trans.addToBackStack(null);
trans.commit();
} else {
Log.d("Drawer Activity","Error in creating Fragment");
}
If the solution doesn't work for you, share the xml code along with your fragment code.
After adding Fragment it will be added to activity state and its view
will be added to defined Container view. But by attaching nothing will
be displayed if fragment was not already added to UI. It just attaches
to fragment manager. However if view was already added to a container
in UI and detached after that, by attaching it will be displayed again
in its container. Finally you can use attach and detach if you want to
destroy fragment View temporarily and want to display and build its
view on future without losing its state inside activity.
https://stackoverflow.com/a/18979024/3329488
My solution is to tag all the fragment with unique tag on fragment replacement. Make sure you also assign a unique tag to the default fragment during it creation. A more efficient way is to identify the fragment before you recreate the same one.
public void selectNavActivty(int position){
// TODO Changing between the different screens selection
FragmentManager fragmentManager = getSupportFragmentManager();
fragment = fragmentManager.findFragmentById(R.id.content_frame);
String fragmentTag = null;
switch (position) {
case 0:
fragmentTag = "case0Tag"; // please change to better tag name
break;
case 1:
fragmentTag = "case1Tag"; // please change to better tag name
break;
default:
Log.d("Drawer Activity","Error in creating Fragment");
return;
}
if (fragmentTag != null && !fragment.getTag().equals(fragmentTag))
fragmentManager.beginTransaction().replace(R.id.content_fragment, fragment, tag).commit();
}
In my case after rotating a device a blank fragment was shown. I understood that in an Activity.onCreate() I always called creating a blank Fragment and after that a needed one. So I changed it's behaviour to this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
openEmptyFragment()
openAnotherFragment()
}
}
I recommend to check savedInstanceState != null before adding new fragments, as written in Why won't Fragment retain state when screen is rotated?.
My question is not easy to describe, but I will do my best:
On my tablet-app I have one activity with a listFragment A on left side and a detailFragment B on right side. So when I click an item on the list, the detailFragment shows the proper details of the chosen (list) item.
Now when I click a button on my detailFragment B. the fragment gets swapped with a new Fragment from type infoFragment. The listFragment on left side stays as it is.
So now, when I click another item on the List, I want the infoFragment to vanish and get a detailFragment once again.
My problem is, that i need some kind of check if currently there is an infoFragment or a detailFragment displayed. So that I can either just refresh the detailFragment OR stop the infoFragment and build a new detailFragment.
idea:
if ( //detailFragment is active ) {
updateContent();
}
else {
FragmentManager.buildDetailFragment();
}
have been fiddling for hours now, any help is appreciated!!
How can i figure it out whether there is a detailFragment or listFragment displayed?
edit:
i change my detailFragment with the infoFragment here:
FragmentManager fm = getFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.details_fragment);
fragment = new InfoFragment();
fm.beginTransaction()
.replace(R.id.details_fragment, fragment)
.commit();
When you add a Fragment to your fragment manager with a FragmentTransaction you can specify a key value. You can then findFragmentByTag which will determine if the Fragment with that key value has been added to the fragment manager.
So long as you are not using a ViewPager or some other structure where multple fragments are added at once, searching for whether your fragment manager contains a fragment by tag will let you know which is currently displayed. You can then use the results of that search to update the fragment since the result is a reference to the fragment itself.
This means you can pass data from the FragmentActivity to the fragment directly by calling any publicly accessable fragment methods. For example
Fragment displayedFragment = fragmentManager.findFragmentByTag(TAG);
if(displayedFragment != null){ //null if no fragment with tag value
displayedFragment.updateList() //public method within fragment
}
MyFragment myFragment = (MyFragment)getFragmentManager().findFragmentByTag("MY_FRAGMENT");
if (myFragment.isVisible()) {
// add your code here
}
From Here
FragmentManager fm = getFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.content_id);
now we can get the fragment name by getClass
fragment.getClass().getSimpleName()
You can get class of fragment and check which one it exactly is by calling getClass().
I am creating a Fragment FragParent displaying several input-fields, one of which is a sub-Fragment frag, with specialized audio controls.
The layouts of both fragments are loaded from XML in their onCreateView() methods.
Code I am using is below, but I have some questions about it:
A) is it OK to replace() the sub-Fragment if it is already returned by findFragmentByTag(), or is that an unnecessary step?
B) can the sub-Fragment be instantiated and replaced in the layout, before the parent Fragment completes the inflate() call?
<!-- language: lang-java -->
// Load sub-Fragment with audio UI.
String fragStr = "fragment_audio_str";
int fragView = R.id.frag_audio_frame_layout;
FragmentManager fm = getChildFragmentManager();
FragmentTransaction fmt = fm.beginTransaction();
Fragment frag = (Fragment) fm.findFragmentByTag(fragStr);
if (null == frag) {
frag = new Fragment();
}
fmt.replace(fragView, frag, fragStr);
fmt.commit();
return inflater.inflate(R.layout.fragment_parent, container, false);
} // END onCreateView() of FragParent
A) is it OK to replace() the sub-Fragment if it is already returned by findFragmentByTag(), or is that an unnecessary step?
No, it shouldn't be necessary to replace the fragment if it is already there. However you need to replace your child fragment if it isn't there.
B) can the sub-Fragment be instantiated and replaced in the layout, before the parent Fragment completes the inflate() call?
No, you need to inflate the parent Fragment layout and then add your child Fragment to it before returning the parent's View.
Try something like this:
View parentFragLayout = inflater.inflate(R.layout.fragment_parent, container, false);
FrameLayout childFragContainer = (FrameLayout) parentFragLayout.findViewById(R.id.frag_audio_frame_layout);
// Load sub-Fragment with audio UI.
String fragStr = "fragment_audio_str";
FragmentManager fm = getChildFragmentManager();
Fragment frag = fm.findFragmentByTag(fragStr);
// Child fragment isn't there, so add it
if (frag == null) {
frag = new Fragment();
FragmentTransaction fmt = fm.beginTransaction();
fmt.replace(childFragContainer.getId(), frag, fragStr);
fmt.commit();
}
return parentFragLayout;
Unless your Fragment is contained your XML layout (which, since you're adding it in the onCreateView, I'm assuming that it is not contained in your XML layout) you will still need to use FragmentManager to add the Fragment. However, you should use add() instead of replace(). You should only call replace() when you are actually replacing another Fragment.
While I'm not a hundred percent sure about the second question, I'm pretty sure that it's a no.