java – BottomNavigationView – How to avoid recreation of Fragments and reuse them-ThrowExceptions

Exception or error:

I would like to make a bottom navigation bar in my project. Every view has it’s own fragment. The problem is that every time i click on the button to change the view for example from recents to favorites it creates new fragment with completely new states(e.g. scroll position, text changed whatever my fragment contains). I know that in official Android documentation there was written that bottom navigation bar should reset the task states, but i think it is too uncomfortable for users.
I would like to have kind of similar functionality like instagram that you change from feed to explore and back to feed the scroll position the image caches everything remains saved. I tried almost every way to solve this problem the only thing that worked is by setting visibility GONE and setting visibility VISIBLE according to situation but i understand that it is not RIGHT way there should be better way of doing this and i am not talking about manually saving needed instances. I followed almost every tutorial about bottom nav fragments but the interesting thing is that no one is interested to make it use without calling new every time.

enter image description here

enter image description here

FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.frameLayout, FirstFragment.newInstance());
fragmentTransaction.commit();

bottomNavigationView = (BottomNavigationView) findViewById(R.id.navigation);
bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        Fragment fragment = null;
        switch (item.getItemId()) {
            case R.id.menu_dialer:
                fragment = FirstFragment.newInstance();
                break;
            case R.id.menu_email:
                fragment = SecondFragment.newInstance();
                break;
            case R.id.menu_map:
                fragment = ThirdFragment.newInstance();
                break;
        }
        if (fragment != null) {
            FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
            fragmentTransaction.replace(R.id.frameLayout, fragment);
            fragmentTransaction.commit();
        }
        return true;
    }
});

I also tried the onAttach and Deattach solutions but again no success.

VIDEO LINK : new i tried Nino Handler version it only works when i tap on the same fragment button

Maybe it is connected that i am using canary version or something wrong in my gradle dependencies?
enter image description here

NEW UPDATES:

NEW UPDATES:

public class MainActivity extends AppCompatActivity {

    private TextView mTextMessage;


    private static final String TAG_FRAGMENT_ONE = "fragment_one";
    private static final String TAG_FRAGMENT_TWO = "fragment_two";

    private FragmentManager fragmentManager;
    private Fragment currentFragment;

    String TAG = "babken";
    private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
            = new BottomNavigationView.OnNavigationItemSelectedListener() {

        Fragment fragment = null;
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            switch (item.getItemId()) {
                case R.id.navigation_home:
                   fragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_ONE);
                    if (fragment == null) {
                        fragment = FragmentFirst.newInstance();
                    }
                    replaceFragment(fragment, TAG_FRAGMENT_ONE);

                    break;
                case R.id.navigation_dashboard:

                     fragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_TWO);
                    if (fragment == null) {
                        fragment = FragmentSecond.newInstance();
                    }
                    replaceFragment(fragment, TAG_FRAGMENT_TWO);

                    break;
            }
            return true;

        }
    };

    private void replaceFragment(@NonNull Fragment fragment, @NonNull String tag) {
        if (!fragment.equals(currentFragment)) {
            fragmentManager
                    .beginTransaction()
                    .replace(R.id.armen, fragment, tag)
                    .commit();
            currentFragment = fragment;
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        fragmentManager = getSupportFragmentManager();
        Fragment fragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_ONE);
        if (fragment == null) {
            fragment = FragmentFirst.newInstance();
        }
        replaceFragment(fragment, TAG_FRAGMENT_ONE);
        BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
    }

}
How to solve:

I had similar issue, but this code solved my problem.

public class MainActivity extends AppCompatActivity {

final Fragment fragment1 = new HomeFragment();
final Fragment fragment2 = new DashboardFragment();
final Fragment fragment3 = new NotificationsFragment();
final FragmentManager fm = getSupportFragmentManager();
Fragment active = fragment1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);


    BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
    navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);

    fm.beginTransaction().add(R.id.main_container, fragment3, "3").hide(fragment3).commit();
    fm.beginTransaction().add(R.id.main_container, fragment2, "2").hide(fragment2).commit();
    fm.beginTransaction().add(R.id.main_container,fragment1, "1").commit();

}


private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
        = new BottomNavigationView.OnNavigationItemSelectedListener() {

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case R.id.navigation_home:
                fm.beginTransaction().hide(active).show(fragment1).commit();
                active = fragment1;
                return true;

            case R.id.navigation_dashboard:
                fm.beginTransaction().hide(active).show(fragment2).commit();
                active = fragment2;
                return true;

            case R.id.navigation_notifications:
                fm.beginTransaction().hide(active).show(fragment3).commit();
                active = fragment3;
                return true;
        }
        return false;
    }
};

Hope this helps.

source : BottomNavigationView With Fragments (No fragment recreation).

###

I wouldn’t keep the fragment instances globally.
Instead add a tag to the fragment when creating them

getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.container, new PlaceholderFragment(), TAG_PLACEHOLDER)
            .commit();

Then you can always retrieve it like this:

Fragment fragment = getSupportFragmentManager().findFragmentByTag(TAG_PLACEHOLDER);
    if (fragment == null) {
        fragment = new PlaceholderFragment();
    }
    getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.container, fragment, TAG_PLACEHOLDER)
            .commit();

UPDATE: I updated my answer and to provide a complete solution:

private static final String TAG_FRAGMENT_ONE = "fragment_one";
private static final String TAG_FRAGMENT_TWO = "fragment_two";
private static final String TAG_FRAGMENT_THREE = "fragment_three";

private FragmentManager fragmentManager;
private Fragment currentFragment;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // instantiate the fragment manager
    fragmentManager = getSupportFragmentManager();

    Fragment fragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_ONE);
    if (fragment == null) {
        fragment = FirstFragment.newInstance();
    }
    replaceFragment(fragment, TAG_FRAGMENT_ONE);

    bottomNavigationView = (BottomNavigationView) findViewById(R.id.navigation);
    bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            Fragment fragment = null;
            switch (item.getItemId()) {
                case R.id.menu_dialer:
                    // I'm aware that this code can be optimized by a method which accepts a class definition and returns the proper fragment
                    Fragment fragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_ONE);
                    if (fragment == null) {
                        fragment = FirstFragment.newInstance();
                    }
                    replaceFragment(fragment, TAG_FRAGMENT_ONE);
                    break;
                case R.id.menu_email:
                    Fragment fragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_TWO);
                    if (fragment == null) {
                        fragment = SecondFragment.newInstance();
                    }
                    replaceFragment(fragment, TAG_FRAGMENT_TWO);
                    break;
                case R.id.menu_map:
                    Fragment fragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_THREE);
                    if (fragment == null) {
                        fragment = ThirdFragment.newInstance();
                    }
                    replaceFragment(fragment, TAG_FRAGMENT_THREE);
                    break;
            }
            return true;
        }
    });
}

private void replaceFragment(@NonNull Fragment fragment, @NonNull String tag) {
    if (!fragment.equals(currentFragment)) {
        fragmentManager
            .beginTransaction()
            .replace(R.id.frameLayout, fragment, tag)
            .commit();
        currentFragment = fragment;
    }
}

ADDITIONAL INFO: If you want to be sure that the fragment states don’t change and if you also want to be able to swipe the fragments you should consider using a ViewPager with a FragmentStatePagerAdapter and change the current fragment in the adapter with every click event

###

All the previous answers are using fragmentTransaction.replace(...). This will replace the current fragment by destroying it (which is causing the problem). Therefore all those solutions won’t actually work.

This is the closest thing I could get to as a solution for this problem:

private void selectContentFragment(Fragment fragmentToSelect)
{
    FragmentTransaction fragmentTransaction = this.getSupportFragmentManager().beginTransaction();

    if (this.getSupportFragmentManager().getFragments().contains(fragmentToSelect)) {
        // Iterate through all cached fragments.
        for (Fragment cachedFragment : this.getSupportFragmentManager().getFragments()) {
            if (cachedFragment != fragmentToSelect) {
                // Hide the fragments that are not the one being selected.
                fragmentTransaction.hide(cachedFragment);
            }
        }
        // Show the fragment that we want to be selected.
        fragmentTransaction.show(fragmentToSelect);
    } else {
        // The fragment to be selected does not (yet) exist in the fragment manager, add it.
        fragmentTransaction.add(R.id.fragment_container, fragmentToSelect);
    }

    fragmentTransaction.commit();
}

To make this work, you should keep track of the fragments in an array (or in separate variables) in your Activity. I for reference pre-instantiated all fragments in a SparseArray.

###

You can use attach() and detach() methods:

private FirstFragment firstFragment = FirstFragment.newInstance();
private SecondFragment secondFragment= SecondFragment.newInstance();
private ThirdFragment thirdFragment = ThirdFragment.newInstance();

navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            switch (item.getItemId()) {
                case R.id.menu_dialer:
                    changeFragment(firstFragment, "firstFragment");
                    return true;
                case R.id.menu_email:
                    changeFragment(secondFragment, "secondFragment");
                    return true;
                case R.id.menu_map:
                    changeFragment(thirdFragment, "thirdFragment");
                    return true;
            }
            return false;
        }
    });

public void changeFragment(Fragment fragment, String tagFragmentName) {

    FragmentManager mFragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

    Fragment currentFragment = mFragmentManager.getPrimaryNavigationFragment();
    if (currentFragment != null) {
        fragmentTransaction.detach(currentFragment);
    }

    Fragment fragmentTemp = mFragmentManager.findFragmentByTag(tagFragmentName);
    if (fragmentTemp == null) {
        fragmentTemp = fragment;
        fragmentTransaction.add(R.id.content, fragmentTemp, tagFragmentName);
    } else {
        fragmentTransaction.attach(fragmentTemp);
    }

    fragmentTransaction.setPrimaryNavigationFragment(fragmentTemp);
    fragmentTransaction.setReorderingAllowed(true);
    fragmentTransaction.commitNowAllowingStateLoss();
}

###

So I have been researching for this for a long time and figured out that there is no way to reuse fragment with AUTOMATICALLY saved states you have to save your needed states manually then retrieve them whenever a new fragment is created but what about scroll position it is too hard and even in some cases it is impossible to save scroll view position state(e.g. recycler view). So i used the concept called VISIBILITY when i click on the button that fragment becomes visible others hide automatically.

###

I wrote a Kotlin Extension function for FragmentManager class

fun FragmentManager.switch(containerId: Int, newFrag: Fragment, tag: String) {

    var current = findFragmentByTag(tag)
    beginTransaction()
        .apply {

            //Hide the current fragment
            primaryNavigationFragment?.let { hide(it) }

            //Check if current fragment exists in fragmentManager
            if (current == null) {
                current = newFrag
                add(containerId, current!!, tag)
            } else {
                show(current!!)
            }
        }
        .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
        .setPrimaryNavigationFragment(current)
        .setReorderingAllowed(true)
        .commitNowAllowingStateLoss()
}

This can be called by supportFragmentManager.swtich(R.id.container,newFrag,newFrag.TAG) from onNavigationItemSelected

###

How about that.
You declare the fragments in the class.

Fragment firstFragment,secondFragment,thirdFragment;

then in the switch-case you can code as that:

switch (item.getItemId()) {
    case R.id.menu_dialer:
        if(firstFragment != null) {
            fragment = firstFragment;
        }else{
            fragment = FirstFragment.newInstance();
        }
        break;
    case R.id.menu_email:
        // the same ...
        break;
    case R.id.menu_map:
        //the same ...
        break;
}

###

Create the three fragments as members of the class and reuse them.

public class MainActivity extends AppCompatActivity {
    private final Fragment mFirstFragment = new FirstFragment();
    private final Fragment mSecondFragment = new SecondFragment();
    private final Fragment mThirdFragment = new ThirdFragment();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...... 
        ......
        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.replace(R.id.frameLayout, mFirstFragment);
        fragmentTransaction.commit();

        bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                if (bottomNavigationView.getSelectedItemId() != item.getItemId()) {
                    switch (item.getItemId()) {
                        R.id.menu_dialer:
                            replaceFragment(mFirstFragment);
                            break;
                        case R.id.menu_email:
                            replaceFragment(mSecondFragment);
                            break;
                        case R.id.menu_map:
                            replaceFragment(mThirdFragment);
                            break;
                    }
                }
                return true;
            }
        });
    }

    private void replaceFragment(Fragment fragment) {
        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.replace(R.id.frameLayout, fragment);
        fragmentTransaction.commit();
    }

}

###

Thomas’s answer nearly helped me but had an issue that whenever I open new fragments the very first time, they overlapped but doesn’t get overlapped once I open them again by pressing the menu buttons.

So I modified his code and obtained the solution using the following code:

 private fun selectContentFragment(fragmentToSelect: Fragment) {
        val fragmentTransaction = fragmentManager?.beginTransaction()
        if (fragmentManager?.fragments?.contains(fragmentToSelect)!!) {
            // Show the fragment that we want to be selected.
            fragmentTransaction?.show(fragmentToSelect)
        } else {
            // The fragment to be selected does not (yet) exist in the fragment manager, add it.
            fragmentTransaction?.add(R.id.container, fragmentToSelect)
        }
        // Iterate through all cached fragments.
        for (cachedFragment in fragmentManager?.fragments!!) {
            if (cachedFragment !== fragmentToSelect) {
                // Hide the fragments that are not the one being selected.
                // Uncomment following line and change the name of the fragment if your host isn't an activity and a fragment otherwise whole view will get hidden.
                // if (!cachedFragment.toString().contains("HomeContainerFragment"))

                fragmentTransaction?.hide(cachedFragment)
            }
        }
        fragmentTransaction?.commit()
    }

Make sure you’re not passing the fragment’s new instance every time.

This will work:

selectContentFragment(
                    when (item.itemId) {
                        R.id.home -> frag1
                        R.id.photoGallery -> frag2
                        else -> Home()
                    }
            )

where frag1 and frag2 are global variables defined as:

 val frag1 = Home()
 val frag2 = PhotoGallery()

This will not work:

selectContentFragment(
                    when (item.itemId) {
                        R.id.home -> Home()
                        R.id.photoGallery -> PhotoGallery()
                        else -> Home()
                    }
            )

It wasted my several hours. Hope it helps others!

Leave a Reply

Your email address will not be published. Required fields are marked *