I am trying to learn Fragments in Android and from various examples I have found there seems to be different ways of doing it and I just wanted to get some advice as to which is the correct way, or at least under which circumstances one way should be used over another.
One example created a layout that contained a fragment and a FrameLayout. In the code, when an item is selected from the ListFragment a new Fragment is created (with some data it requires in the constructor) and the FrameLayout is replaced with this new Fragment (using FragmentTransaction.replace()).
Another example has a layout file that declares the 2 fragments side by side. Now in the code when the user selects an item from the list in one fragment a call is made to the other fragment to update the data (based on the selected item).
So I am just wondering if either of these methods is preferred over the other or if there are certain circumstances where one should be used?
EDIT: here is the code for each of the two methods I was referring to:
1:
mCurCheckPosition = index;
if (mDualPane) {
// We can display everything in-place with fragments, so update
// the list to highlight the selected item and show the data.
getListView().setItemChecked(index, true);
// Check what fragment is currently shown, replace if needed.
DetailsFragment details = (DetailsFragment)
getFragmentManager().findFragmentById(R.id.details);
if (details == null || details.getShownIndex() != index) {
// Make new fragment to show this selection.
details = DetailsFragment.newInstance(index);
// Execute a transaction, replacing any existing fragment
// with this one inside the frame.
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.details, details);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
} else {
// Otherwise we need to launch a new activity to display
// the dialog fragment with selected text.
Intent intent = new Intent();
intent.setClass(getActivity(), DetailsActivity.class);
intent.putExtra("index", index);
startActivity(intent);
}
2:
public void onListItemClick(ListView l, View v, int position, long id) {
String item = (String) getListAdapter().getItem(position);
DetailFragment fragment = (DetailFragment) getFragmentManager()
.findFragmentById(R.id.detailFragment);
if (fragment != null && fragment.isInLayout()) {
fragment.setText(item);
} else {
Intent intent = new Intent(getActivity().getApplicationContext(),
DetailActivity.class);
intent.putExtra("value", item);
startActivity(intent);
}
}
So I am just wondering if either of these methods is preferred over the other or if there are certain circumstances where one should be used?
If the actual fragment does not need to change (i.e., it is the same fragment class), I would have the activity call a method on that fragment rather than replace it (your scenario #2), assuming it exists. That's much less expensive at runtime, and it's probably simpler to code as well.
If, however, the fragment might need to be a different one (e.g., depending on what you click, there may be different fragments for different types of model objects represented in the list), then replacing the fragment will be needed (your scenario #1). You could optimize the case where the fragment happens for this event to be of the same class, though I'd focus first on getting it working just by replacing the fragment and worry about the optimization if/when you have the time and inclination.
I'm not a fan of your #2 code structurally, though. IMHO, fragments should not be talking with other fragments directly. My preferred pattern is for fragments to "stick to their knitting", focusing solely on things within their own widgets and models. For events that affect other parts of the UI (e.g., list click), have the fragment notify the activity (e.g., via a listener interface). The activity is the one that knows which fragments should be around, as it is the one that created them in the first place. The activity can then either talk to the other fragment (if it exists), create the other fragment (if there is room), or start up another activity. If you prefer your #2 approach, you are welcome to use it -- it's just not what I'd do in your circumstance.
Related
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.
I've found a number of questions that are similar, but I haven't found any answers that seems to fit my specific case
What is the proper way of iterating through all of the fragments on the backstack, in order to perform a specific operation on each of them? I need to update some and remove some of the fragments, based on changes in the environment (I could, theoretically, trap an event when the fragment receives focus again and take appropriate action then, but this would complicate things a bit, as I'm also dealing with things getting renamed)
Given that the container for the fragments is as follows:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
And adding fragments as follows:
FragmentTransaction transaction = context.getSupportFragmentManager().beginTransaction();
Fragment fragment = new CustomFragment();
transaction.add(R.id.fragment_container, fragmentBase);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
transaction.addToBackStack(nameOfFragment);
transaction.commit();
context.getSupportFragmentManager().executePendingTransactions();
My first attempt to iterate was:
getSupportFragmentManager().executePendingTransactions();
for (int indexFragment = 0; indexFragment < getSupportFragmentManager().getBackStackEntryCount(); indexFragment++)
{
FragmentManager.BackStackEntry backStackEntry = getSupportFragmentManager().getBackStackEntryAt(indexFragment);
// If Android keeps the ID, surely there should be a way of getting to the actual fragment itself, from that ID?
Fragment fragment = getSupportFragmentManager().findFragmentById(backStackEntry.getId());
if (fragment != null)
{
//TODO: Cast the fragment and perform some transactions against it
// We never get here; as fragment is always null
}
}
From a different angle:
FrameLayout frameLayout = (FrameLayout)findViewById(R.id.fragment_container);
for (int indexFrameLayout = 0; indexFrameLayout < frameLayout.getChildCount(); indexFrameLayout++)
{
View view = (View)frameLayout.getChildAt(indexFrameLayout);
// I get the right number of views; but I can't interact with them as fragments
}
I could, hypothetically, hack my way past this issue by keeping references to the fragments, but there are problems with that approach.
Is there a way that I can get to the fragments that are currently on the backstack?
Given the assumption that I can get to a fragment, is there a way of removing it (and it alone) from somewhere within the backstack?
(There is the transaction.remove method, but was that intended to work on fragments sitting in the middle of the back stack?)
Thanks
BackStackEntry is not necessary associated with single Fragment, it represents a FragmentManager state, you may have a bunch of fragments in a transaction and therefore a bunch of fragments in a state. Use this flavor of add() method and assign an unique id to your fragments via tag. Then you'll be able to find them via getSupportFragmentManager().findFragmentById(fragment_tag);
As for your task, afaik there's no way to remove an arbitrary state from BackStack, your only choice is to pop stack removing one state after the other from the top. So you can, for example, override your onBackPressed() and call fragmentManager.popBackStack() to simply go to the previous state or fragmentManager.popBackStack(backstack_tag) to skip some states and go where you need to.
I have an activity that have some buttons and some fragments.
If I click on button A, i'll show Fragment "FragA". When I'm in "FragA", I can perform some actions like choose a picture from gallery and I need to stay in "FragA" after choose picture.
But when I choose picture, I return to Activity and "FragA" is hidden.
How can perform an action and still in same Fragment or display correct fragment in Activity?
You can recreate fragment again and replace it in your Activity with using modification of this code:
if (currentState == STATE_MAIN_FRAGMENT) {
return;
}
mainScreenFragment = (MainScreenFragment) getSupportFragmentManager().findFragmentByTag(MainScreenFragment.TAG);
if (mainScreenFragment == null) {
mainScreenFragment = new MainScreenFragment();
}
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.flFragmentContainer, mainScreenFragment, MainScreenFragment.TAG);
fragmentTransaction.commit();
First "if" checks if the fragment is set or not. It is not required but it's a good practice. It prevents you from replacing fragment when it is not necessary.
And there is one thing strange for me. Because you said <<"FragA" is hidden>> - that means it was already set but container is not visible? Then yourFragmentContainer.setVisiblity(View.VISIBLE); in on Activity result.
And the last thing that could help you is to retain the fragment so it won't be ever destroyed and recreated again. Some helpful links:
Understanding Fragment's setRetainInstance(boolean)
http://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean)
Or you can just copy-paste what is in your Button's OnClickListener so it happens onActivityResult too.
So I am trying to get some experience with Fragments, but I'm finding some roadblocks.
My current situation is as follows.
I have an activity that displays a List whose content is determined by Extra Intent parameters sent from the 'calling' activity.
This List activity uses ListFragment declared in the XML like so:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="#color/black">
<fragment class="com.pixlworks.NLC.DirectoryBrowse$ListingFragment"
android:id="#+id/listing"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Currently I get the parameter that indicates the type of content directly in the Fragment by accessing the Extra data of the Activity Intent (or saved Bundle if available):
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
mListingType = savedInstanceState.getString(Utils.DIRECTORY_TYPE_STORE_KEY);
else
mListingType = getActivity().getIntent().getStringExtra(Utils.DIRECTORY_TYPE_STORE_KEY);
// get content by type, create and set the adapter
}
Now part of my problem is that I am not sure this is the right way to 'pass' that parameter from the Activity to the Fragment.
On top of that, I am getting issues with this setup when using the Action Bar's UP Navigation. When I click on an item in this List Activity it goes to another activity showing the details of the selected item. From this detail activity:
If I use the back button, the List Activity is brought back from the stack as usual and everything works fine.
If I use the ActionBar's UP (despite following steps here), it would seem that a new instance is created instead of using the one in the stack and this new instance obviously is not getting the Extra parameter in the Intent. Since I am expecting the value to exist in the saved Bundle or in the Intent, my app crashes in this situation.
So to boil things down, I am not sure which of these to follow and how to make them work properly with 'UP' navigation:
A) Hold the 'type' parameter in a field in the Activity and save it in the Activity's Bundle onSaveInstanceState. In which case I am not sure how to then pass the value to the Fragment. In this case I would just need to make sure that UP calls the existing instance of the Activity List
B) Continue with my current setup of saving the value in the Fragment instead of the Activity, but again, how to handle the UP navigation correctly?
I know it is kind of multiple things I am asking here at the same time, but they are all connected, so I hope that I can get some help on this.
Thanks for any help in advance!
The UP navigation makes more sense to be used within the same activity level. That is the intention of the codes that you followed in the developers page. Because you started a new activity, if you want to return to previous activity like the back button you will need to call finish() to destroy the details activity first.
As for passing data from activity to fragment, when you create a new instance of fragment, you can pass the data to it as bundle, for example:
// in fragment class
public static MyFragment newInstance(Bundle arg) {
MyFragment f = new MyFragment();
f.setArguments(arg);
return f;
}
When you create a new fragment, you can call:
// in activity
Bundle arg = new Bundle();
int info = ...;
arg.putInt("INFO",info);
...
MyFragment mFragment = MyFragment.newInstance(arg);
Finally, to get the data in fragment:
int info = getArguments().getInt("INFO");
...
Instead of directly calling MyFragment mFragment = new MyFragment() to instantiate the fragment, you should use a static method to instantiate it. This is to prevent some crashes which might happen if you rotate the screen and the framework complains that it couldn't find a public empty constructor.
UPDATE
To answer your questions:
1) Say you start from activity A -> activity B. Then in activity B you press the up button. By logic of use, the up button will not bring you back to activity A, because its intention is to navigate one level up,but still inside, activity B. To return to activity A, you need to call finish() to destroy activity B first.
2) If your fragment is created in xml, you still can set arguments. In your xml, you set an id for the fragment android:id="#+id/fragment_id", then
// in activity
FragmentManager fm = getSupportFragmentManager(); // or getFragmentManager() if you don't have backward compatibility
MyFragment mFragment = fm.findFragmentById(R.id.fragment_id);
Bundle arg = new Bundle();
// put data blah blah
mFragment.setArguments(arg);
Just make sure you set the arguments before you use the fragment.
Simply said, intent is used when you pass data between calling activities; bundle is used when you want to pass data from activity to fragment.
I have added a background service to my application which creates a notificaion when a new item is added to my application. When pressing the notification the user is taken into the application and the intent passes an object which allows the application to select the newly added item.
The application is for both mobile phones and tablets. When running on phones the item is shown in a separate activity, when on a tablet a dual fragment layout is used and the item is shown on the right fragment.
In the main activity onCreate I check the intent and check if a item has been passed through and display it if it has. This is working fine on the phone but on a tablet the right fragment is not visible and hence the item can not be shown.
This is what I call at the end of onCreate (I had tried it in onStart and onResume)
Bundle data = queryIntent.getExtras();
if (data!=null){
Deal deal = data.getParcelable("notificationDeal");
if (deal!=null){
onDealSelected(deal);
}
}
The method onDealSeletced does the following
public void onDealSelected(Deal deal) {
if (!mDualFragments){
Intent showDealDetails = new Intent(getApplicationContext(), DealDetailsActivity.class);
showDealDetails.putExtra("Deal", deal);
showDealDetails.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(showDealDetails);
Log.d("OnDealSelected", "1");
}
else{ // must be tablet
if (dealDetailsFragment == null)
dealDetailsFragment = (DealDetailsFragment) getFragmentManager().findFragmentByTag("dealDetailsFragment");
if (!dealDetailsFragment.isVisible()){
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.right_fragment_container, dealDetailsFragment);
transaction.setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
transaction.commit();
getFragmentManager().executePendingTransactions(); // ensure it is done before we call update deal!
Log.d("OnDealSelected", "2");
}
if (dealDetailsFragment.isVisible()) {
dealDetailsFragment.updateDeal(deal);
Log.d("OnDealSelected", "3");
}
}
}
On a smartphone mDualFragments is false and hence it shows the deal in a new activity and works as expected.
When on a tablet it goes into the else, however it never gets into the final if as the fragment is not visible.
When running the application on a tablet it goes into the second if but after it the fragment is still not visible.
The same method is used at other points in the application (when a deal is not passed through in the intent) and has been working as expected.
You can use setArguments(Bundle bundle) to pass data to the fragment before it is attached (before the commit action). This way when the Fragment initializes itself it can call getArguments and parse the bundle. This way you don't have to worry about the fragment being visible yet, it can create its views when ready. There is a full example in the Fragment Docs
Try the transaction.add() method and hide the previous fragment. I suppose your fragment will be visible now.
transaction.add(R.id.right_fragment_container, dealDetailsFragment);