50

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?

Tester101
  • 8,042
  • 13
  • 55
  • 78
  • If you are willing to dump Navigation drawer, you can make do with Viewpager. Swiping sideways won't destroy the fragments. However it may not be effective solution. – Dhaval Mar 28 '14 at 12:58

12 Answers12

55

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();
    }
}
Tester101
  • 8,042
  • 13
  • 55
  • 78
  • Is the onDestroy necessary have you found out? – Lion789 Jun 30 '15 at 10:52
  • Check out this post on how to get the currently visible fragment if you have more than two fragments. I like Matt Mobrea's answer: http://stackoverflow.com/questions/9294603/get-currently-displayed-fragment – Micro Feb 22 '16 at 14:57
  • Thank you for writing up your solution. Would you mind also sharing your xml file? How is the element behind `R.id.container` declared? – Christian May 17 '16 at 08:16
  • @Christian R.id.container is likely just a LinearLayout, however, this question/answer is two years old. The code no longer exists, and is likely no longer relevant. Sorry. – Tester101 May 17 '16 at 09:45
  • How is this code no longer relevant? Have you got a better solution? – Tushar Kathuria Jun 08 '16 at 16:06
  • @TusharKathuria No I don't have a better solution, but I imagine the operating system has changed quite a bit in two years. – Tester101 Jun 08 '16 at 17:03
  • 2
    Actually I just tested it and it worked very fine. So yes it still works. – Tushar Kathuria Jun 08 '16 at 17:10
  • @Lion789 - There is no point in the `onDestroy` work, since the OS is killing EVERYTHING in your app at that time. – ToolmakerSteve Sep 20 '16 at 11:29
  • 1
    Would just like to mention that according to the documentation: Note: When you remove or replace a fragment and add the transaction to the back stack, the fragment that is removed is stopped (not destroyed). If the user navigates back to restore the fragment, it restarts. If you do not add the transaction to the back stack, then the fragment is destroyed when removed or replaced. -- https://developer.android.com/training/basics/fragments/fragment-ui.html – Burkely91 Jul 31 '17 at 13:19
  • what if we have 4 fragments ? – Karthic Srinivasan Jan 13 '19 at 14:26
  • @KarthicSrinivasan just expand the switch statement to account for the additional fragments. – Tester101 Jan 20 '19 at 15:06
  • its better answer! tnx – roghayeh hosseini Aug 18 '19 at 11:25
  • this link is benefit also: "Switching Between Fragments Without the Mindless Killing Spree" in medium. – roghayeh hosseini Aug 18 '19 at 11:26
  • if we have more than 2 fragment, how to hide all of other fragments? – roghayeh hosseini Aug 18 '19 at 11:47
  • @Lion789 it is. if you remove onDestroy, fragments will remain in memory – fisio Jul 04 '20 at 19:08
19

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();
}
Patrick
  • 33,984
  • 10
  • 106
  • 126
  • Would there be any drawbacks that you can foresee with using this option over the other suggestions of hiding/showing selected fragments? To me it seems as if there would be slightly less overhead in this option rather than maintaining the state & UI of a fragment. – Burkely91 Jul 31 '17 at 12:35
  • 5
    Android may gc the unused fragment if detached, if you hide/show it you would have a strong reference to it, so this method is more memory efficient. – Patrick Jul 31 '17 at 13:49
  • gc? Sorry, relative newb – Burkely91 Jul 31 '17 at 14:55
  • @Burkely91Garbage collector - automatically cleans up unused resources – Otziii Jun 12 '20 at 10:32
  • Thank you, using attach and detach is better in my case, because findFragmentById gives the latest attached fragment. So if I use show/hide, I can't get the visible fragment since the hidden fragments are still attached to the container. – rasitayaz Jun 30 '20 at 18:42
5

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.

meredrica
  • 2,563
  • 1
  • 21
  • 24
4

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)

Adam Toth
  • 933
  • 2
  • 13
  • 27
2

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.

Drew
  • 3,307
  • 22
  • 33
  • 2
    IMHO, this doesn't answer the question. Rambling on about lifecycle of fragments. Then a hint that maybe retained fragments would be useful - though no guidance as to how. – ToolmakerSteve Sep 20 '16 at 11:35
2

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();
    }
  • Yes, the solution was to use the .show and .hide methods. In the original code I was using .replace, which destroys the fragment. – Tester101 May 17 '18 at 18:09
1

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();
0

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
Hossein
  • 797
  • 1
  • 8
  • 24
0

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)
    }
}
Adam Johns
  • 35,397
  • 25
  • 123
  • 176
0

Simplest way

Just replace this code with your's

var transition = getSupportFragmentManager().beginTransaction();
var added_frags = getSupportFragmentManager().getFragments();
var isAdded = false;

for (Fragment frag : added_frags) {
    if (frag.getClass() == fragment.getClass()) {
        isAdded = true;
        transition.show(frag);
    } else {
        transition.hide(frag);
    }
}

if (!isAdded) {
    transition.add(R.id.container, fragment);
}

transition.commit();

fragment is the Fragment you want to replace.

ʀᴀʜɪʟ
  • 235
  • 1
  • 4
  • 8
-1

How about playing with the Visible attribute?

shinjidev
  • 383
  • 1
  • 6
  • 21
-2

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