BottomNavigationView - How to avoid recreation of Fragments and reuse them

Viewed 29107

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);
    }

}
14 Answers

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

public class MainActivity extends AppCompatActivity {

    boolean doubleBackToExitPressedOnce = false;
    final Fragment fragment1 = new HomeFragment();
    final Fragment fragment2 = new DashboardFragment();
    final Fragment fragment3 = new NotificationsFragment();
    final FragmentManager fm = getSupportFragmentManager();
    Fragment active = fragment1;
    BottomNavigationView navigation;

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

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        navigation = (BottomNavigationView) findViewById(R.id.navigation);
        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
        setFragment(fragment1, "1", 0);
    }


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

        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            switch (item.getItemId()) {
                case R.id.navigation_home:
                    setFragment(fragment1, "1", 0);
                    return true;
                case R.id.navigation_dashboard:
                    setFragment(fragment2, "2", 1);
                    return true;
                case R.id.navigation_notifications:
                    setFragment(fragment3, "3", 2);
                    return true;
            }
            return false;
        }
    };

    public void setFragment(Fragment fragment, String tag, int position) {
        if (fragment.isAdded()) {
            fm.beginTransaction().hide(active).show(fragment).commit();
        } else {
            fm.beginTransaction().add(R.id.main_container, fragment, tag).commit();
        }
        navigation.getMenu().getItem(position).setChecked(true);
        active = fragment;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_settings) {
            startActivity(new Intent(MainActivity.this, SettingsActivity.class));
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onBackPressed() {
        if (active == fragment1) {
            if (doubleBackToExitPressedOnce) {
                super.onBackPressed();
                return;
            }
            this.doubleBackToExitPressedOnce = true;
            Toast.makeText(this, "Please click BACK again to exit", Toast.LENGTH_SHORT).show();
        } else {
            setFragment(fragment1, "1", 0);
        }
    }
}

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

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

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!

**This code very helpful for me. Just like youtube. **

private Deque<Integer> fragmentIds = new ArrayDeque<>(3);
int itemId;
private HomeFragment homeFragment = new HomeFragment();
private FavouriteFragment favouriteFragment = new FavouriteFragment();
private NearmeFragment nearmeFragment = new NearmeFragment();
BottomNavigationView bottomNavigationView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    fragmentIds.push(R.id.action_home);
    showTabWithoutAddingToBackStack(homeFragment);
    bottomNavigationView = findViewById(R.id.bottom_navigation);
    bottomNavigationView.setOnNavigationItemSelectedListener(onNavigationItemClicked);
}

private BottomNavigationView.OnNavigationItemSelectedListener onNavigationItemClicked = new BottomNavigationView.OnNavigationItemSelectedListener() {
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        itemId= item.getItemId();
        if(fragmentIds.contains(itemId)){
            fragmentIds.remove(itemId);
        }
        fragmentIds.push(itemId);
        showTabWithoutAddingToBackStack(getFragment(item.getItemId()));
        return true;
    }
};

private Fragment getFragment(int fragmentId) {
    switch (fragmentId) {
        case R.id.action_home:
            return homeFragment;
        case R.id.action_favorites:
            return favouriteFragment;
        case R.id.action_nearme:
            return nearmeFragment;
    }
    return homeFragment;
}

private void showTabWithoutAddingToBackStack(Fragment fragment) {
    getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment, fragment.getClass().getSimpleName()).commit();
}

@Override
public void onBackPressed() {
    if(fragmentIds.getLast() != R.id.action_home){
            fragmentIds.addLast(R.id.action_home);
    }
    fragmentIds.pop();
    bottomNavigationView.getMenu().getItem(fragmentIds.size()-1).setChecked(true);
    if (!fragmentIds.isEmpty()) {
        showTabWithoutAddingToBackStack(getFragment(fragmentIds.peek()));
    } else {
        finish();
    }
}

For Kotlin user may help this code :

First create a FragmentManager extension class

fun FragmentManager.replace(containerId: Int, fragment: 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 = fragment
            add(containerId, current!!, tag)
        } else {
            show(current!!)
        }
    }
    .setTransition(FragmentTransaction.TRANSIT_ENTER_MASK)
    .setPrimaryNavigationFragment(current)
    .setReorderingAllowed(true)
    .commitNowAllowingStateLoss()

}

Now simply call it on your

 onNavigationItemSelected



supportFragmentManager.replace(R.id.fragment_id,YourFragmentClass,yourFragment.TAG) 

I had the same issue but I resolved it by creating multiple fragment hosts then I added fragments in fragmentHost. And when bottom nav itemSelected I just make the required host fragment visible and other host fragmentsHost to visibility gone.

This method may be wrong but it works perfectly for me. And we don't have to manually handle the states of fragment only we need to handle backpress of activity. But this method does not pause the fragments.

And I don't know how to handle the pauses and resumes of fragments so please reply to me.

Maybe This will help you.

This is my fragmentHosts Home Activity:

<androidx.fragment.app.FragmentContainerView
        android:id="@+id/homeFragmentHost"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/bottom_NavBar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:visibility="gone"/>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/libraryFragmentHost"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/bottom_NavBar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:visibility="gone"/>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/myStuffFragmentHost"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/bottom_NavBar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:visibility="gone"/>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/moreFragmentHost"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/bottom_NavBar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:visibility="gone"/>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_NavBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/app_bottom_nav_menu"
        tools:ignore="BottomAppBar" />

Home Activity onCreate:

supportFragmentManager.beginTransaction().replace(R.id.homeFragmentHost,HomeFragment()).commitNow()
    supportFragmentManager.beginTransaction().replace(R.id.libraryFragmentHost,LibraryFragment()).commitNow()
    supportFragmentManager.beginTransaction().replace(R.id.myStuffFragmentHost,MyStuffFragment()).commitNow()
    supportFragmentManager.beginTransaction().replace(R.id.moreFragmentHost,MoreFragment()).commitNow()
    homeFragmentHost.visibility = View.VISIBLE

Bottom nav Item Selected listener

    override fun onNavigationItemSelected(item: MenuItem): Boolean {
    return when(item.itemId){
        R.id.homeFragment -> loadFragmentHost(homeFragmentHost)
        R.id.libraryFragment -> loadFragmentHost(libraryFragmentHost)
        R.id.myStuffFragment -> loadFragmentHost(myStuffFragmentHost)
        R.id.moreFragment -> loadFragmentHost(moreFragmentHost)
        else -> false
    }

loadFragmentHost function

    private fun loadFragmentHost(view:FragmentContainerView): Boolean {
    val list = arrayListOf(homeFragmentHost,libraryFragmentHost,myStuffFragmentHost,moreFragmentHost)
    list.remove(view)
    view.visibility = View.VISIBLE
    list.forEach {
        it.visibility = View.GONE
    }
    return true
}
public class MainActivity extends AppCompatActivity {

    boolean doubleBackToExitPressedOnce = false;
    final Fragment fragment1 = new HomeFragment();
    final Fragment fragment2 = new DashboardFragment();
    final Fragment fragment3 = new NotificationsFragment();
    final FragmentManager fm = getSupportFragmentManager();
    Fragment active = fragment1;
    BottomNavigationView navigation;

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

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        navigation = (BottomNavigationView) findViewById(R.id.navigation);
        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
        setFragment(fragment1, "1", 0);
    }


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

        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            switch (item.getItemId()) {
                case R.id.navigation_home:
                    setFragment(fragment1, "1", 0);
                    return true;
                case R.id.navigation_dashboard:
                    setFragment(fragment2, "2", 1);
                    return true;
                case R.id.navigation_notifications:
                    setFragment(fragment3, "3", 2);
                    return true;
            }
            return false;
        }
    };

    public void setFragment(Fragment fragment, String tag, int position) {
        if (fragment.isAdded()) {
            fm.beginTransaction().hide(active).show(fragment).commit();
        } else {
            fm.beginTransaction().add(R.id.main_container, fragment, tag).commit();
        }
        navigation.getMenu().getItem(position).setChecked(true);
        active = fragment;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_settings) {
            startActivity(new Intent(MainActivity.this, SettingsActivity.class));
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onBackPressed() {
        if (active == fragment1) {
            if (doubleBackToExitPressedOnce) {
                super.onBackPressed();
                return;
            }
            this.doubleBackToExitPressedOnce = true;
            Toast.makeText(this, "Please click BACK again to exit", Toast.LENGTH_SHORT).show();
        } else {
            setFragment(fragment1, "1", 0);
        }
    }
}
  1. Maintain Bottom sheet fragment Reusable
  2. BackPress Maintain
  3. double back press to exit
public class MainActivity extends AppCompatActivity {
    BottomNavigationView bottomNavigationView;
    Toaster toaster;
    private final Fragment androidFragment = new AndroidFragment();
    private final Fragment settingsFragment = new SettingsFragment();
    Fragment active;    
    String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    toaster = new Toaster(this);
    bottomNavigationView = findViewById(R.id.bottom_navigation);
    renderFragment(androidFragment, "android");
    active = androidFragment;
    bottomNavigationView.setOnItemSelectedListener(
            item -> {
                Log.e(TAG, "onCreate: " + active );
                if(item.getItemId() == R.id.action_android){
                    renderFragment(androidFragment, "android");
                    return true;
                }
                else if(item.getItemId() == R.id.action_settings){
                    renderFragment(settingsFragment, "settings");
                    return true;
                }
                return false;
            }
    );
}

private void renderFragment(Fragment fragment, String tag){
    FragmentManager fragmentManager = getSupportFragmentManager();
    if(fragment.isAdded()){
        fragmentManager.beginTransaction().hide(active).show(fragment)
                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).commit();
    }
    else {
        if(active != null){
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.fragment_container_view, fragment, tag)
                    .hide(active)
                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).commit();
        }
        else {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.fragment_container_view, fragment, tag)
                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).commit();
        }
    }
    active = fragment;
}
}

This worked for me Guys.

Related