Android Fragment declaring in activity xml OR newInstance() factory method? - android

In my project I have an activity and multiple fragments.
Currently the fragments are declared in my activity xml
e.g:
<fragment
tools:layout="#layout/fragment_do_you_know"
android:name="myapp.fragments.DoYouKnowFragment"
android:id="#+id/doYouKnowFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"/>
<fragment
tools:layout="#layout/fragment_whats_new"
android:name="myapp.fragments.WhatsNewFragment"
android:id="#+id/whatsNewFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"/>
During activity initialization I get fragment references:
mWhatsNewFragment = (WhatsNewFragment) mFragmentManager.findFragmentById(R.id.whatsNewFragment);
mDoYouKnowFragment = (DoYouKnowFragment) mFragmentManager.findFragmentById(R.id.doYouKnowFragment);
On different actions I show one of the fragment and hide all other in FragmentManager transaction:
protected void showFragment(BaseFragment fragment) {
FragmentTransaction transaction = mFragmentManager.beginTransaction();
transaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
BaseFragment visibleFragment = null;
for (BaseFragment fr: mAllFragments) {
if (fragment == fr) {
transaction.show(fr);
visibleFragment = fr;
}
else {
transaction.hide(fr);
}
}
transaction.commit();
if (visibleFragment != null) {
visibleFragment.onShow();
}
}
I know that the other approach is to use newInstance() factory method for getting the fragment refereces.
In that case I suppose I have to set the layout parameters (layout_width, layout_height) by code.
But I think this is the right way if I want to pass initialization paramters to fragment.
So I wonder which approach is better.
And also is keeping references to all fragments is Ok or is better creating during transaction?

Not at all. when you are creating newInstance factory method you do so because you want to pass some arguments from activity to fragment. normally you would do it with constructor but thats not an option when working with fragments. so thats only reason to create factory method for fragments other times you would just call default constructor. now in either case that doesnt mean that you will need to write layout paramets in code. there is nice workaround for that. you will create FrameLayout or any ViewGroup and set its layout parameters in xml. now at some point when you will want to add your fragment you can just add your fragment(or replace) in that ViewGroup. code is as simple as anything can get.
supportFragmentManager
.beginTransaction()
.replace(R.id.your_view_group_id, BadAssFragment.newInstance(someCoolData))
.commit()

Related

Unable to pass data to fragment through Bundle

I'm learning android fundamentals and I came across this problem while creating my first app. I have an activity which passes on data to a fragment. The OnCreate method of the activity has a block like this:
if(savedInstanceState == null){
DetailActivityFragment detailFrag = DetailActivityFragment.newInstance(movieId);
getSupportFragmentManager().beginTransaction().add(android.R.id.content,detailFrag).commit();
}
setContentView(R.layout.activity_detail);
At the fragment (activity_detail) if I perform getParameters(), I receive null. By playing around, I found that if I remove setContentView method from the snippet above, the fragment shows up with the data. Any ideas as to why that was a problem? Thanks!
Edit: Here is my static newInstance method in the fragment
public static DetailActivityFragment newInstance(String id) {
DetailActivityFragment fragment = new DetailActivityFragment();
Bundle args = new Bundle();
args.putString(Intent.EXTRA_TEXT, id);
fragment.setArguments(args);
return fragment;
}
Here's my fragment from the layout activity_detail:
<fragment android:name="app.appone.DetailActivityFragment"
android:id="#+id/fragment_detail"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
You have to pass the data to your fragment.
Create a static method on your fragment for instance creation. It should look like this:
public static newInstance(Object param) {
DetailActivityFragment yourFragment = new DetailActivityFragment();
Bundle args = new Bundle();
args.put(key, value);
yourFragment.setArguments(args);
return yourFragment;
}
And in your onCreate method of the fragment you can get that data using the method "getArguments();
Your activity code is ok. But I would prefer using "replace" instead of "add" method.
Your latest edit shows you are using a static fragment in your layout xml, but creating it dynamically. A static fragment is created in your xml file:
<fragment android:name="app.appone.DetailActivityFragment"
android:id="#+id/fragment_detail"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Whereas a dynamic fragment is generated in your code with FragmentManager. It makes sense that calling setContentView() would cause a conflict, as the fragment you are creating with FragmentManager is being replaced by the fragment you are defining in your xml file. The one in your xml, unlike your dynamic fragment, has no arguments, which is why it's returning null.
As you use android.R.id.content, you can remove this static fragment from your xml completely. Replace it with an empty layout, such as FrameLayout, and set an id attribute. Then, when using FragmentManager, replace android.R.id.content for this id.
For example:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/frag_container" />
And in your Activity file:
DetailActivityFragment frag = (DetailActivityFragment) getSupportFragmentManager().findFragmentById(R.id.frag_container);
if (frag == null) {
frag = DetailActivityFragment.newInstance(id);
getSupportFragmentManager()
.beingTransaction()
.add(R.id.frag_container, frag)
.commit();
}
Thanks for your edit. I think you are using the wrong id for fragment replacement.
As in a previous comment you should first set the content view. Your layout file should have a placeholder view, e.g. Framelayout. Give your layout an id and reference this id in your replacement code.
Your "R.layout.activity_detail" should have a layout snippet like this:
<FrameLayout id="+#id/my_detail_frag"/>
And your activity code should look like this:
getSupportFragmentManager().beginTransaction().add(R.id.my_detail_frag,detailFrag).commit();
This answer will do the trick for you:
Best practice for instantiating a new Android Fragment
You should use setArguments() and getArguments() to pass the Bundle into the Fragment.
Good luck!

findFragmentByTag() returns null after perform a FragmentTransaction using replace() method

My Android app consists three fragments: A, B and C. They're loaded in the two containers defined in the MainActivity layout.
When the app is started, it shows the fragmentA loaded in the left_container and the fragmentC in the right_container.
If you press the button in the fragmentA, a FragmentTransaction changes FragmentC by FragmentB.
At the moment everything OK. But the trouble appears when I try to get a reference to the loaded fragmentB using findFragmentByTag(), because it returns null. I've used the method replace in the FragmentTransaction and I've finished it with commit(), but there isn't way to call FragmentB method. My code:
MainActivity.java:
public class MainActivity extends Activity{
static String fragmentTag = "FRAGMENTB_TAG";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Adds the left container's fragment
getFragmentManager().beginTransaction().add(R.id.left_container, new FragmentA()).commit(); //Adds the fragment A to the left container
//Adds the right container's fragment
getFragmentManager().beginTransaction().add(R.id.right_container, new FragmentC()).commit(); //Adds the Fragment C to the right container
}
/**
* Called when the button "Activate Fragment B" is pressed
*/
public void buttonListener(View v){
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.right_container, new FragmentB(),fragmentTag); //Replaces the Fragment C previously in the right_container with a new Fragment B
ft.commit(); //Finishes the transaction
//!!HERE THE APP CRASHES (java.lang.NullPointerException = findFragmentByTag returns null
((FragmentB) getFragmentManager().findFragmentByTag(fragmentTag)).testView();
}
}
FragmentB.java:
public class FragmentB extends Fragment {
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_b, container,false);
}
/**
* Gets a reference to the text_fragment_b TextView and calls its method setText(), changing "It doesn't work" text by "It works!"
*/
public void testView(){
TextView tv = (TextView)getView().findViewById(R.id.text_fragment_b);
tv.setText("It works!");
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<FrameLayout android:id="#+id/left_container" android:layout_width="0px" android:layout_weight="50" android:layout_height="match_parent"/>
<FrameLayout android:id="#+id/right_container" android:layout_width="0px" android:layout_weight="50" android:layout_height="match_parent"/>
</LinearLayout>
fragment_b.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="5sp">
<TextView
android:id="#+id/text_fragment_b"
android:text="It doesn't works!"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
Please help me! I'm a beginner in Android development!
I've fixed it! I called getSupportFragmentManager().executePendingTransactions() after doing the transaction and it worked! After calling that method I can get the fragment using both findFragmentById() and findFragmentByTag() methods.
if you use setRetainInstance(true) than you can't use findFragmentByTag() in onCreate from the Activity. Do it at onResume
see the documentation: setRetainInstance
I'll start by apologising since I'm still very new myself...
I think the problem may be in the declaration of the fragmentTag static String not properly getting access from the class's instances, just change that line to:
private final static String FRAGMENT_TAG = "FRAGMENTB_TAG"; // using uppercase since it's a constant
Also, I would be more explicit when declaring instances, for example:
public void buttonListener(View v){
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.right_container, new FragmentB(), FRAGMENT_TAG);
ft.commit();
FragmentB fragB = (FragmentB) getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
fragB.testView();
}
I hope you get this sorted, as I seen this question posted earlier and was surprised that it hadn't got any activity yet.
Also, here are a couple of links to the android documentation on replace:
Android Training - Replace
Android Reference - Replace
I had the same problem and realized that there is a really simple way to fix this. When using a tag please do make sure to add the
fragmentTransaction.addToBackStack(null);
method so that your Fragment is resumed instead of destroyed as mentioned in the developer guides.
If you don't call addToBackStack() when you perform a transaction that removes a fragment, then that fragment is destroyed when the transaction is committed and the user cannot navigate back to it. Whereas, if you do call addToBackStack() when removing a fragment, then the fragment is stopped and is later resumed if the user navigates back.
You can find this at the end of this section.
Every time I tried to reference back to my created Fragment, it turns out it had already been destroyed so I lost about 30 minutes trying to figure out why my Fragment was not being found through a simple findFragmentByTag(); call.
Hope this helps!
Be sure you are adding or replacing the fragment in the proper way
Next statement will add the fragment but it will return null when using getFragmentManager().findFragmentByTag(tag):
transaction.add(R.id.mainContent, fragment);
This way it will work;
transaction.add(R.id.mainContent, fragment, tag);
We are also seeing this problem but the cause is slightly different. The suggested solution by https://stackoverflow.com/a/21170693/1035008 doesn't work for us.
void updateFragment(Fragment newFragment) {
FragmentTransaction ft = getFragmentManager().beginTransaction();
// We have these 2 lines extra
Fragment current = getChildFragmentManager().findFragmentByTag(fragmentTag);
if (current != null) { ft.remove(current); }
ft.replace(R.id.right_container, newFragment, fragmentTag); //Replaces the Fragment C previously in the right_container with a new Fragment B
ft.commit(); //Finishes the transaction
//!!HERE THE APP CRASHES (java.lang.NullPointerException = findFragmentByTag returns null
((FragmentB) getFragmentManager().findFragmentByTag(fragmentTag)).testView();
}
And after reading the documentation about replace:
Replace an existing fragment that was added to a container. 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.
I realize that the remove call was not necessary since it is done by replace automatically. So after delete ft.remove(current), it works fine.
In my case I used the code to replace and add to BackStack, but set wrong tag:
val fragment = { SomeFragment.newInstance() }
fragmentManager?.replaceAndAddToBackStack(R.id.container, fragment, WrongAnotherFragment.TAG)
Of course, supportFragmentManager.findFragmentByTag(SomeFragment.TAG) didn't find SomeFragment.
For me probably it was a newbie mistake that I was calling super.onCreate(savedInstanceState); after I was trying to access the Fragment using findFragmentByTag.
I moved super.onCreate(savedInstanceState) up in the order and it started working for me.

How to replace Fragments of different types?

Good morning. I'm developing an app in which I have a main layout that extends from activity and inside this one I have one fragment of one data type, in my case FragmentCover (it's a class).
During my app, I push a button and I want to change this fragment layout for another layout that extends from fragment but of different type, called SongList.
My problem is that I have defined this fragment for the class of Cover and when I change I don't have any problem, but when I want to get the views and set to one variable of my class, the funciont songList = (ListView) getView().findViewById(R.id.songList); it's null and it gives me error.
I put it here what I do.
layout
<fragment android:name="es.xxx.ui.FragmentCover"
android:id="#+id/pruebaa"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_above="#id/songBar"
android:layout_below="#id/header"/>
mainclass change
public void onClick(View v) {
if(isCoverFragment){
FragmentSongList fragmentSongList = new FragmentSongList();
transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.pruebaa, fragmentSongList);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
transaction.addToBackStack("LIST");
transaction.commit();
isCoverFragment=false;
}
else{
transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.pruebaa, fragmentCover);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
transaction.addToBackStack("COVER");
transaction.commit();
getSongCover();
isCoverFragment=true;
}
The problem is not the change, is when I try to make the findViewById, it's probabbly because it doesn't load the view associated with FragmentSongList.
You need to use a FrameLayout inside your layout file, instead of defining the Fragment in .xml
Into that FrameLayout, you can inflate any Fragment you want.
Just inflate the First Fragment you want to be displayed directly in your onCreate(...) method.
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
This is how to inflate the Fragments programatically into the FrameLayout.
FragmentSongList fragmentSongList = new FragmentSongList();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.content_frame, fragmentSongList);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
transaction.addToBackStack("LIST");
transaction.commit();
You can leave the code inside your onClick() method just the way it is, just change the id to "content_frame". Furthermore, as mentioned above you will have to inflate the first Fragment that should be displayed inside your onCreate(...) method.

Fragments displayed over each other

I've got an Activity with a DrawerLayout, using the guidelines from http://developer.android.com/training/implementing-navigation/nav-drawer.html.
When I click on an drawerItem, I replace the current view with the new fragment:
Fragment fragment;
Bundle args = new Bundle();
fragment = new NewsListFragment();
args.putInt("category", position);
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.content_frame, fragment)
.commit();
mDrawerList.setItemChecked(position, true);
Now, sometimes the old fragment is not replaced but the new fragment is placed on top of the old one:
http://img15.imageshack.us/img15/3179/1kqj.png
Why is this, and how to solve this problem?
Relevant XML:
<!-- The main content view -->
<LinearLayout
android:id="#+id/rlMain"
android:layout_width="fill_parent"
android:orientation="vertical"
android:layout_height="fill_parent">
<FrameLayout
android:id="#+id/content_frame"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">
</FrameLayout>
This only happens sometimes, and I haven't found a flow to reproduce this yet. The app doesn't support rotating, so it won't happen there.
We went live with this version and havent received any complaints about this, so I will assume this was the correct answer:
in your onCreateView method add:
if (container != null) {
container.removeAllViews();
}
Be sure to check if container is not null!
Thanks https://stackoverflow.com/users/2677588/lia-pronina!
After about 1 week, I found the solution without adding background color or anything else. Just add this code and fix that bullshit. I hope it will help all of you.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
container.clearDisappearingChildren();
return inflater.inflate(R.layout.fragment, container, false);
}
Add in the every layout
android:background="#FFFFFF"
The layouts background in default are transparen, so just put a background color and the new elements fragment, not display over old fragment
let's try with
fragmentManager.beginTransaction().executePendingTransactions();
after you call commit() method
I run in this same problem and I see that there's already an accepted answer but these answer is not 100% right and didn't fix my problem.
The proposed answer by #Niels removes the views but the fragment(s) is(are) still added.
This is what I am using:
/**
* Call this to remove all the other added fragments and keep only the current one.
*
* #param activity the activity to which the fragment has been attached.
* #param fragment the fragment we want to keep.
*/
public static void removeOtherAddedFragments(#NonNull AppCompatActivity activity, #NonNull Fragment fragment) {
FragmentManager fragmentManager = activity.getSupportFragmentManager();
for (Fragment frag : fragmentManager.getFragments()) {
if (frag != null && !frag.equals(fragment) && frag.isAdded()) {
fragmentManager.beginTransaction().remove(frag).commit();
}
}
}
I am calling this in my onResume to be sure that it will be called also when I navigate back to the fragment.
I also encounter this issue I found that we are doing a wrong when replacing fragment
private void changeFragment(Fragment targetFragment){
assert getFragmentManager() != null;
getFragmentManager()
.beginTransaction()
.replace(R.id.main_fragment, targetFragment, "fragment")
.setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
}
this method is replace fragment as other people's code. the reason why fragments are display each other is we define different ID in framelayout in xml. we have to define framelayout IDs(old fragment and new fragment) same.
old framelayout and new framelayout ID in xml for above code should be
<FrameLayout
android:layout_width="match_parent"
android:id="#+id/main_fragment"
android:layout_height="match_parent"/>
Let's say that you have the root fragment A which you add to a container. Next you add fragment B and you set addToBackStack in the transaction. Lastly you add fragment C but you omit addToBackStack. Now when you press the back button you get these fragments on top of each other.
To recap:
Fragment A is added
Fragment B replaces A with addToBackStack
Fragment C replaces B without addToBackStack
Pressing back results in weird overlaid fragments where A lies on top of C.
You could solve this issue by also adding C to the backStack.

How to replace the activity's fragment from the fragment itself?

My application has a Fragment inside its Activity. I would like to programmatically replace the fragment by another one from the current fragment itself.
For example, if I click on a button inside the fragment, the fragment should be replaced with another one, but the activity should remain the same.
Is it possible? If so, how to do it?
It's actually easy to call the activity to replace the fragment.
You need to cast getActivity():
((MyActivity) getActivity())
Then you can call methods from MyActivity, for example:
((MyActivity) getActivity()).replaceFragments(Object... params);
Of course, this assumes you have a replaceFragments() method in your activity that handles the fragment replace process.
Edit: #ismailarilik added the possible code of replaceFragments in this code with the first comment below which was written by #silva96:
The code of replaceFragments could be:
public void replaceFragments(Class fragmentClass) {
Fragment fragment = null;
try {
fragment = (Fragment) fragmentClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flContent, fragment)
.commit();
}
from the official docs:
// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
In this example, newFragment replaces whatever fragment (if any) is currently in the layout container identified by the R.id.fragment_container ID. By calling addToBackStack(), the replaced fragment is saved to the back stack so the user can reverse the transaction and bring back the previous fragment by pressing the Back button.
The behavior you have described is exactly what fragments are designed to do. Please go through the official guide for a thorough understanding of fragments which will clear up all your questions.
http://developer.android.com/guide/components/fragments.html
Please note that fragment should NOT directly replace itself or any other fragments. Fragments should be separate entities. What fragment should do is to notify its parent activity that some event has happened. But it is, again, NOT a fragment job to decide what to do with that! It should be activity to decide to i.e. replace the fragment on phone, but to i.e. add another to existing one on tablets. So you are basically doing something wrong by design.
And, as others already mentioned, your activity should use FragmentManager ("native" or from compatibility library) to do the job (like replace() or add() or remove()):
http://developer.android.com/guide/components/fragments.html
Just as Marcin said, you shouldn't have a fragment start another fragment or activity. A better way to handle this situation is by creating a callback implementation for the main activity to handle requests such as start a new fragment. Here is a great example in the android developer guide.
There is a way which works; Just (in the fragment) do the following:
getFragmentManager().beginTransaction()
.add(R.id. container_of_this_frag, new MyNewFragment())
.remove(this)
.commit();
When using nested fragments, we don't want every inner fragment replacement goes to the outer most activity. A mechanism allowing a fragment to notify its parent that it wants to change to another fragment can be useful.
Here is my code in Kotlin, I think it is easy to translate into java.
interface FragmentNavigator {
fun navigateTo(fragment: Fragment)
}
class NavigableFragment: Fragment() {
var navigator: FragmentNavigator? = null
override fun onDetach() {
super.onDetach()
navigator = null
}
}
Inner fragments need to extend NavigableFragment, and use following code to change itself to another fragment.
navigator?.navigateTo(anotherFragment)
Outer activities or fragments need to implement FragmentNavigator, and override navigateTo.
override fun navigateTo(fragment: Fragment) {
supportFragmentManager.beginTransaction().replace(view_id, fragment).commit()
}
//Use childFragmentManager instead of supportFragmentManager a fragment
Finally in outer activities or fragments, override onAttachFragment
override fun onAttachFragment(fragment: Fragment?) {
super.onAttachFragment(fragment)
if(fragment is NavigableFragment) {
fragment.navigator = this
}
}
This worked for me:
getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container,
new MenuFragment()).commit();
For Kotlin.
(activity as YourActivityLauncherFragment)
.supportFragmentManager.beginTransaction()
.replace(R.id.yourFragmentContainer, YourFragmentName()).setReorderingAllowed(true)
.commit()

Categories

Resources