I have one Activity with Fragment:
onCreate()
{
getSupportFragmentManager().beginTransaction()
.add(R.id.container, PostsGaleryFragment.newInstance(), FRAGMENT_TAG).addToBackStack("home")
.commit();
}
After user's action I call:
public void onShowPostRequested(ShowPost pShowPost)
{
SinglePostFragment singlePostFragment = SinglePostFragment.newInstance(pShowPost.getPostId());
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, singlePostFragment, FRAGMENT_TAG)
.addToBackStack(null)
.commit();
}
Then navigation can be made backward to home:
#Override
public void onBackPressed()
{
FragmentManager supportFragmentManager = getSupportFragmentManager();
if (supportFragmentManager.getBackStackEntryCount() > 0)
{
supportFragmentManager.popBackStack();
} else
{
super.onBackPressed();
}
}
Problem:
There is more than one instance of "home" Fragment. That is not good for me as every one of those fragments keep lot of references to quite big bitmaps and there is an OOM error waiting just behind the corner.
Questions:
Why old instance of Fragment is not used after .popBackStack()?
I temporary made a workaround with something like singleton pattern - works ok for now, but are there any disadvantages I should be aware of?
i guess what you are missing is check for onSavedInstance, try same as below
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, PostsGaleryFragment.newInstance(), FRAGMENT_TAG).addToBackStack("home")
.commit();
}
onCreate()
{
FragmentManager fm = getSupportFragmentManager();
PostGalleryFragment f = fm.findFragmentByTag(FRAGMENT_TAG);
if(f == null) {
f = fm.beginTransaction().add(R.id.container,
PostsGaleryFragment.newInstance(), FRAGMENT_TAG).commit();
}
Related
My activity is composed of 3 nested Fragments. There is my MainFragment that is displayed by default, ProductFragment that can be called from it, then DetailFragment can be called from ProductFragment.
I can go back and forth between my ProductFragment and DetailFragment. By doing so, the popStackBack method is accumulating similar fragments. Then, if I click on the back button, It will go back through all the Fragments as many time I called them.
What is the proper way to avoid the same Fragment to be kept in the back stack ?
EDIT :
I firstly call my main fragment :
if (savedInstanceState == null) {
getFragmentManager()
.beginTransaction()
.add(R.id.container, new SearchFragment(), "SEARCH_TAG")
.commit();
}
Here is the code that calls the fragments from the activity :
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.setCustomAnimations(R.animator.enter_from_bottom, R.animator.exit_to_top, R.animator.enter_from_bottom, R.animator.exit_to_top);
ft.replace(R.id.container, new FactFragment(), "FACT_TAG");
ft.addToBackStack("FACT_TAG");
ft.commit();
Then, on back click :
#Override
public void onBackPressed() {
getFragmentManager().popBackStack();
}
I tried to get the tag of my current fragment and execute some specific code related to it but it doesn't work well. I also tried to addToBackStack() only when current Fragment wasn't already added to the backStack but it messed up my fragment view.
Use fragment's method isAdded() to evaluate the insertion. For example:
if(!frag.isAdded()){
//do fragment transaction and add frag
}
Here is my solution. Maybe dirty but it works. I implemented a method that returns the tag of the fragment that is displayed before clicking the on back button :
public String getActiveFragment() {
if (getFragmentManager().getBackStackEntryCount() == 0) {
return null;
}
String tag = getFragmentManager()
.getBackStackEntryAt(getFragmentManager()
.getBackStackEntryCount() - 1)
.getName();
return tag;
}
Then, on my onBackPressed() method :
// Get current Fragment tag
String currentFrag = getActiveFragment();
if(currentFrag.equals("PRODUCT_TAG")) {
// New transaction to first Fragment
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.setCustomAnimations(R.animator.enter_from_right, R.animator.exit_to_left, R.animator.enter_from_right, R.animator.exit_to_left);
ft.replace(R.id.container, new SearchFragment(), "MAIN_TAG");
ft.commit();
} else {
// Go to Fragment-1
getFragmentManager().popBackStack();
}
Here is my handy and simple solution to check for duplicate insertion through fragment manager
at first, I check if it is first time intention for adding fragment and then I check if the fragment is presented using fragment manager
Fragment fragment = getSupportFragmentManager().findFragmentByTag("firstFragment");
if (fragment == null) {
getSupportFragmentManager().beginTransaction().add(R.id.frameLayout, new FirstFragment(), "firstFragment")
.addToBackStack(null)
.commit();
}else if(!fragment.isAdded()){
getSupportFragmentManager().beginTransaction().add(R.id.frameLayout, new FirstFragment(), "firstFragment")
.addToBackStack(null)
.commit();
}
Here is my solution:
Fragment curFragment = fragmentManager.findFragmentById(R.id.frameLayout);
if(curFragment != null
&& curFragment.getClass().equals(fragment.getClass())) return;
// add the fragment to BackStack here
Xamarin.Android (C#) version:
var curFragment = fragmentManager.FindFragmentById(Resource.Id.frameLayout);
if (curFragment != null
&& curFragment.GetType().Name == fragment.GetType().Name) return;
// add the fragment to BackStack here
I'm working on an android application, that uses a navigation drawer to switch between two fragments. However, each time I switch, the fragment is completely recreated.
Here is the code from my main activity.
/* The click listener for ListView in the navigation drawer */
private class DrawerItemClickListener implements ListView.OnItemClickListener {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectItem(position);
}
}
private void selectItem(int position) {
android.support.v4.app.Fragment fragment;
String tag;
android.support.v4.app.FragmentManager; fragmentManager = getSupportFragmentManager();
switch(position) {
case 0:
if(fragmentManager.findFragmentByTag("one") != null) {
fragment = fragmentManager.findFragmentByTag("one");
} else {
fragment = new OneFragment();
}
tag = "one";
break;
case 1:
if(fragmentManager.findFragmentByTag("two") != null) {
fragment = fragmentManager.findFragmentByTag("two");
} else {
fragment = new TwoFragment();
}
tag = "two";
break;
}
fragment.setRetainInstance(true);
fragmentManager.beginTransaction().replace(R.id.container, fragment, tag).commit();
// update selected item and title, then close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mNavTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
I've set up some debug logging, and every time selectItem is called, one fragment is destroyed, while the other is created.
Is there any way to prevent the fragments from being recreated, and just reuse them instead?
After #meredrica pointed out that replace() destroys the fragments, I went back through the FragmentManager documentation. This is the solution I've come up with, that seems to be working.
/* The click listener for ListView in the navigation drawer */
private class DrawerItemClickListener implements ListView.OnItemClickListener {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectItem(position);
}
}
private void selectItem(int position) {
android.support.v4.app.FragmentManager; fragmentManager = getSupportFragmentManager();
switch(position) {
case 0:
if(fragmentManager.findFragmentByTag("one") != null) {
//if the fragment exists, show it.
fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("one")).commit();
} else {
//if the fragment does not exist, add it to fragment manager.
fragmentManager.beginTransaction().add(R.id.container, new OneFragment(), "one").commit();
}
if(fragmentManager.findFragmentByTag("two") != null){
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("two")).commit();
}
break;
case 1:
if(fragmentManager.findFragmentByTag("two") != null) {
//if the fragment exists, show it.
fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("two")).commit();
} else {
//if the fragment does not exist, add it to fragment manager.
fragmentManager.beginTransaction().add(R.id.container, new TwoFragment(), "two").commit();
}
if(fragmentManager.findFragmentByTag("one") != null){
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("one")).commit();
}
break;
}
// update selected item and title, then close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mNavTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
I also added this bit, but I'm not sure if it's necessary or not.
#Override
public void onDestroy() {
super.onDestroy();
FragmentManager fragmentManager = getSupportFragmentManager();
if(fragmentManager.findFragmentByTag("one") != null){
fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("one")).commit();
}
if(fragmentManager.findFragmentByTag("two") != null){
fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("two")).commit();
}
}
Use the attach/detach method with tags:
Detach will destroy the view hirachy but keeps the state, like if on the backstack; this will let the "not-visible" fragment have a smaller memory footprint. But mind you that you need to correctly implement the fragment lifecycle (which you should do in the first place)
Detach the given fragment from the UI. This is the same state as when it is put on the back stack: the fragment is removed from the UI, however its state is still being actively managed by the fragment manager. When going into this state its view hierarchy is destroyed.
The first time you add the fragment
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.add(android.R.id.content, new MyFragment(),MyFragment.class.getSimpleName());
t.commit();
then you detach it
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.detach(MyFragment.class.getSimpleName());
t.commit();
and attach it again if switched back, state will be kept
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.attach(getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()));
t.commit();
But you always have to check if the fragment was added yet, if not then add it, else just attach it:
if (getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()) == null) {
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.add(android.R.id.content, new MyFragment(), MyFragment.class.getSimpleName());
t.commit();
} else {
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.attach(getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()));
t.commit();
}
The replace method destroys your fragments. One workaround is to set them to Visibility.GONE, another (less easy) method is to hold them in a variable. If you do that, make sure you don't leak memory left and right.
I did this before like this:
if (mPrevFrag != fragment) {
// Change
FragmentTransaction ft = fragmentManager.beginTransaction();
if (mPrevFrag != null){
ft.hide(mPrevFrag);
}
ft.show(fragment);
ft.commit();
mPrevFrag = fragment;
}
(you will need to track your pervious fragment in this solution)
I guess you can not directly manipulate the lifecycle mechanisms of your Fragments. The very fact that you can findFragmentByTag is not very bad. It means that the Fragment object is not recreated fully, if it is already commited. The existing Fragment just passes all the lifecycle steps each Fragment has - that means that only UI is "recreated".
It is a very convenient and useful memory management strategy - and appropriate, in most cases. Fragment which is gone, has the resources which have to be utilized in order to de-allocate memory.
If you just cease using this strategy, the memory usage of your application could increase badly.
Nonetheless, there are retained fragments, which lifecycle is a bit different and do not correspond to the Activity they are attached to. Typically, they are used to retain some things you want to save, for example, to manage configuration changes
However, the fragment [re]creation strategy depends on the context - that is, what you would like to solve, and what are the trade-offs that you are willing to accept.
Just find the current fragment calling getFragmentById("id of your container") and then hide it and show needed fragment.
private void openFragment(Fragment fragment, String tag) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Fragment existingFragment = fragmentManager.findFragmentByTag(tag);
if (existingFragment != null) {
Fragment currentFragment = fragmentManager.findFragmentById(R.id.container);
fragmentTransaction.hide(currentFragment);
fragmentTransaction.show(existingFragment);
}
else {
fragmentTransaction.add(R.id.container, fragment, tag);
}
fragmentTransaction.commit();
}
Same idea as Tester101 but this is what I ended up using.
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Fragment oldFragment = fragmentManager.findFragmentByTag( "" + m_lastDrawerSelectPosition );
if ( oldFragment != null )
fragmentTransaction.hide( oldFragment );
Fragment newFragment = fragmentManager.findFragmentByTag( "" + position );
if ( newFragment == null )
{
newFragment = getFragment( position );
fragmentTransaction.add( R.id.home_content_frame, newFragment, "" + position );
}
fragmentTransaction.show( newFragment );
fragmentTransaction.commit();
Hide easily in kotlin using extensions:
fun FragmentManager.present(newFragment: Fragment, lastFragment: Fragment? = null, containerId: Int) {
if (lastFragment == newFragment) return
val transaction = beginTransaction()
if (lastFragment != null && findFragmentByTag(lastFragment.getTagg()) != null) {
transaction.hide(lastFragment)
}
val existingFragment = findFragmentByTag(newFragment.getTagg())
if (existingFragment != null) {
transaction.show(existingFragment).commit()
} else {
transaction.add(containerId, newFragment, newFragment.getTagg()).commit()
}
}
fun Fragment.getTagg(): String = this::class.java.simpleName
Usage
supportFragmentManager.present(fragment, lastFragment, R.id.fragmentPlaceHolder)
lastFragment = fragment
Here's what I'm using for a simple 2 fragment case in Kotlin:
private val advancedHome = HomeAdvancedFragment()
private val basicHome = HomeBasicFragment()
override fun onCreate(savedInstanceState: Bundle?) {
...
// Attach both fragments and hide one so we can swap out easily later
supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.fragment_container_view, basicHome)
add(R.id.fragment_container_view, advancedHome)
hide(basicHome)
}
binding.displayModeToggle.onStateChanged {
when (it) {
0 -> swapFragments(advancedHome, basicHome)
1 -> swapFragments(basicHome, advancedHome)
}
}
...
}
With this FragmentActivity extension:
fun FragmentActivity.swapFragments(show: Fragment, hide: Fragment) {
supportFragmentManager.commit {
show(show)
hide(hide)
}
}
How about playing with the Visible attribute?
this is a little late response.
if you're using view pager for fragments, set the off screen page limit of the fragment to the number of fragments created.
mViewPager.setOffscreenPageLimit(3); // number of fragments here is 3
I've got the next code to switch between 4 fragments in a container [Main, A, B, C].
I need back button to go back to [Main] no matter how the user has navigated through fragments. For example, if I go [Main] >> [A] >> [C] when pressing back should go to [Main].
But I'm not getting the desired result. I think that I'm not doing well the coparisson between fragments.
Launcher MainFragment = new Launcher();
public void switchFragment(Fragment pFragment) {
FragmentManager fm = getSupportFragmentManager();
Fragment currentFragment = fm.findFragmentById(R.id.fragment_container);
if (pFragment == MainFragment){
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, pFragment).commit();
}
else if (currentFragment == MainFragment && pFragment != MainFragment){
//Fragment fr = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.fragment_container, pFragment).addToBackStack(null).commit();
}
else {
//Fragment fr = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.fragment_container, pFragment).commit();
}
currentFragment = pFragment;
}
UPDATE--
I've seen this way is working better, but still makes issue. If I navigate through some fragments without returning back to the Main fragment, when I press back it doesn't go back. It's like if there was some issue with the popbackstack().
public void switchFragment(Fragment pFragment) {
FragmentManager fm = getSupportFragmentManager();
Fragment currentFragment = fm.findFragmentById(R.id.fragment_container);
if (pFragment.equals(MainFragment)){
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, pFragment).commit();
}
else if (currentFragment.equals(MainFragment) && !pFragment.equals(MainFragment)){
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.fragment_container, pFragment).addToBackStack(null).commit();
}
else {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.fragment_container, pFragment).commit();
}
}
This functionality can be achieved by removing .addToBackStack from your fragment transactions.
Here is the Android reference for back stack functionality within fragments: http://developer.android.com/training/implementing-navigation/temporal.html#back-fragments
I finally get the dessired result just replacing the fragment I want on the back_key onClickListener:
final OnClickListener goBack = new OnClickListener() {
#Override
public void onClick(View v) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, MainFragment).commit();
}
};
This way, if In the future I need to go back to another fragment instead of the Main, I'll just add a condition to compare fragment tags and do something depending the tag.
I have a problem with a fragment. I need to display a fragment many times and don't create new instant of it. I have a method that it use to change the content of activity.
protected void setContentFragment(Fragment contentFragment) {
this.contentFragment = contentFragment;
setContentView(R.layout.content_frame);
getSupportFragmentManager().beginTransaction()
.replace(R.id.content_frame, contentFragment).commit();
getSlidingMenu().showContent();
}
content_frame is a simple layout.
<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" />
The problem is: I create instant of the first Fragment and pass it to setContentFragment(...) (still keep instant of this). Then call setContentFragment with other Fragment. And now, I pass instant of the first Fragment to this method, it show bank screen. Please help me :(
Try This..
private void switchFragment(Fragment fragment) {
if (getActivity() == null)
return;
if (getActivity() instanceof MainActivity) {
DashboardActivity dashboard = (MainActivity) getActivity();
dashboard.switchFragment(fragment, "Main List");
}
}
do this
protected void setContentFragment(Fragment contentFragment) {
this.contentFragment = contentFragment;
Random random = new Random(100);
String rString = "myRandomString"+random.nextInt(); // Create a random string here everytime you call setContentFragment
setContentView(R.layout.content_frame);
getSupportFragmentManager().beginTransaction()
.replace(R.id.content_frame, contentFragment, rString).commit();
getSlidingMenu().showContent();
}
This will replace the fragment as you require
Reference: http://developer.android.com/reference/android/support/v4/app/FragmentTransaction.html#replace%28int,%20android.support.v4.app.Fragment,%20java.lang.String%29
When swapping fragments you need to detach/reattach instead of replacing them. This leaves it in memory. Also you don't need to setContentView again, just leave the existing FrameLayout ( you could also use android.R.id.content for the main activity content view, instead of creating a FrameLayout)
Fragment content1, content2;
Fragment current=content1;
public void onCreate() {
if(savedInstanceState==null)
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, content1)
.commit();
}
public void swap() {
if(current==content1) {
getSupportFragmentManager().beginTransaction()
.detach(content1)
.attach(android.R.id.content, content2)
.commit();
current=content2;
} else {
getSupportFragmentManager().beginTransaction()
.detach(content2)
.attach(android.R.id.content, content1)
.commit();
current=content1;
}
}
I am having trouble figuring out the proper way to navigate through fragments without a pager and i am having problems during Configuration changes for screen orientation. I am using Show/Hide on the fragments to make them visible and functional but i am wondering if i should instead be using Detach/Attach. I am also having problems adding things to the back stack and i think it is also due to the use of show/hide. Is it better to use Attach/detatch or is there a way to override what the back button does to make it show/hide the last/current fragment.
The Behavior:
I have a map fragment and a List fragment along with a few others. everything starts up correctly and works initially with orientation changes. When i navigate to the list view it populates correctly but upon orientation change the list gets redrawn without the Data in it. The map view also gets redrawn and is visible behind my pager title indicator.
If anyone could please point me in right direction for solving this that would be awesome. I am suspecting that is is caused by the way that i am showing and hiding the fragments.
Here is where i create the Fragments and add them to the fragment manager. I have also shown where i show/hide fragments.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map_frags);
mapViewContainer = LayoutInflater.from(this)
.inflate(R.layout.map, null);
setupFragments();
showFragment(0);
}
public void setListData(String name) {
bName = name;
showFragment(1);
}
private void setupFragments() {
final FragmentManager fm = getSupportFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
mFragment1 = fm.findFragmentByTag("f1");
if (mFragment1 == null) {
mFragment1 = new MenuFragment();
ft.add(mFragment1, "f1");
ft.hide(mFragment1);
}
mMapFragment = (MapFragment) getSupportFragmentManager()
.findFragmentByTag(MapFragment.TAG);
if (mMapFragment == null) {
mMapFragment = MapFragment.newInstance(0);
ft.add(R.id.fragment_container, mMapFragment, MapFragment.TAG);
}
ft.hide(mMapFragment);
myListFragment = (ListFrag) getSupportFragmentManager()
.findFragmentByTag(ListFrag.TAG);
if (myListFragment == null) {
myListFragment = new ListFrag();
ft.add(R.id.fragment_container, myListFragment, ListFrag.TAG);
}
ft.hide(myListFragment);
frag = (frag) getSupportFragmentManager().findFragmentByTag(
frag.TAG);
if (frag == null) {
bacFrag = new frag();
ft.add(R.id.fragment_container, frag, frag.TAG);
}
ft.hide(bacFrag);
ft.commit();
}
public void showFragment(int fragIn) {
final FragmentTransaction ft = getSupportFragmentManager()
.beginTransaction();
ft.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
if (mVisible != null) {
if (mVisible == mListFragment) {
ft.remove(mListFragment);
} else {
ft.hide(mVisible);
}
}
switch (fragIn) {
case 0:
ft.show(mMapFragment);
ft.commit();
mVisible = mMapFragment;
break;
case 1:
mListFragment = (ListFragmentDisplay) getSupportFragmentManager()
.findFragmentByTag(ListFragmentDisplay.TAG);
Toast.makeText(this, "startListFrag", Toast.LENGTH_LONG).show();
if (mListFragment == null) {
mListFragment = new ListFragmentDisplay();
ft.add(R.id.fragment_container, mListFragment,
ListFragmentDisplay.TAG);
}
ft.show(mListFragment).commit();
mVisible = mListFragment;
break;
case 2:
ft.show(myfragment).commit();
mVisible = myfragment;
break;
case 3:
ft.show(frag).commit();
mVisible = frag;
break;
}
}
It's not your fault. The problem is that when the orientation changes all the Activity is Destroyed, even all the fragments added. So none of the data within it is retained.
It's not advised to use android:configChanges="orientation|keyboardHidden".
Rather, set for every fragment setRetainInstance(true) and it will work well with your current code.
If you want to have a better persistence (for example when the activity is temporarily destroyed for space issues) also remember to save the state of your fragments with onSaveInstanceState. setRetainInstance will work only when a configuration change is about to come.