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.
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 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.
Does the fragment in it receive onDetach, onDestroy method? Or do I need to remove fragment first and then clear the view?
It doesn't matter the order you do it as far I understand it. If you clear the view, the fragment's elements will be removed from the activity's view but you still need to remove it from the fragment manager.
For example, I am creating something like the following in my activity:
ViewGroup viewFragments = (ViewGroup)findViewById(R.id.layout_fragments);
viewFragments.removeAllViews();
View child = ViewGroup.inflate(mActivity, R.layout.child_edit_item, null);
viewFragments.addView(child);
Fragment f = new MyFragment();
FragmentManager mgr = mActivity.getSupportFragmentManager();
FragmentTransaction trans = mgr.beginTransaction();
if (mgr.findFragmentByTag(p.getKey()) != null) {
trans.replace(R.id.fragment_container, f, p.getKey());
} else {
trans.add(R.id.fragment_container, f, p.getKey());
}
trans.commit();
How can I reset or reload a fragment container, to make it empty.
I have a master detail view and I want to reset the detail container to empty on a menu item click.This works in some cases and does not in some.
NullFragment fragment = new NullFragment();
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
fragmentTransaction.replace(R.id.item_detail_container,
fragment);
int count = fragmentManager.getBackStackEntryCount();
fragmentManager.popBackStackImmediate(count, 0);
fragmentTransaction.commit();
Usually you simply remove the fragment from it.
For example do something like
getFragmentManager().beginTransaction().remove(getFragmentManager().findFragmentById(R.id.your_container)).commit();
this will remove the fragment from the your_container holding it.
This gets the fragment currently present in your_container
getFragmentManager().findFragmentById(R.id.your_container)
and this remove the fragment
getFragmentManager().beginTransaction().remove(fragment).commit();
EDIT
Also sometimes it is useful to ensure all transactions are performed and finished, this can be done by using
getFragmentManager().executePendingTransactions();
I want to add a youtube fragment into my already existing fragments dynamically. The code I used is below:
// setting the Youtube Player Dynamically
private int setYoutubePlayer(String desc, View view, int prevID,
Bundle input) {
if (desc.indexOf("=") != -1) {
desc = desc.substring(desc.indexOf("=") + "=".length());
} else {
return prevID;
}
final String url = desc;
LinearLayout videoLayout = new LinearLayout(view.getContext());
videoLayout.setOrientation(LinearLayout.VERTICAL);
prevID++;
videoLayout.setId(prevID);
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
fragment.setVideoId(url);
LinearLayout itemLayout = (LinearLayout) view.findViewById(R.id.items);
itemLayout.addView(videoLayout);
fragmentTransaction.add(itemLayout.getId(), fragment,
"youtube fargment " + prevID);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
return prevID;
}
I need to get the youtube fragment in the appropriate fragment. As I checked when always a new fragment get loaded (when swipe between fragments), the new inner fragment needs to be the first loaded fragment.
Any help will be gladly accepted.
SOLVED: Thank you Koby You were right. i had to replace "getActivity().getSupportFragmentManager();" with "getChildFragmentManager()". The problem was apparently the Sherlock library came with a old android v4 support library. I had to update the support library in the Sherlock. It worked for me.....
to create a nested fragment inside a fragment, you should use:
http://developer.android.com/reference/android/app/Fragment.html#getChildFragmentManager()
call the getChildFragmentManager() from the parent fragment,
and do the transaction in the parent to nest the child inside.
https://stackoverflow.com/a/13381825/1693674
tell me if you need more help in doing that...