I have Activity with five fragments which switch by pressing button in BottomNavigationView:
bottomNavigationView.setOnNavigationItemSelectedListener(item -> {
switch (item.getItemId()) {
case R.id.first:
replaceFragment(FIRST_TAG);
return true;
case R.id.other:
// last four fragments
default:
return false;
}
});
in replaceFragment:
private void replaceFragment(#NonNull String tag) {
if (!checkFragment(tag)) {
addFragment(getFragmentByTag(tag), tag);
}
showFragment(tag);
}
сheckFragment:
private boolean checkFragment(String tag) {
return fm.findFragmentByTag(tag) != null;
}
addFragment:
private void addFragment(Fragment fragment, String tag) {
if (!checkFragment(tag)) {
fm.beginTransaction().add(R.id.frame, fragment, tag).hide(fragment).commit();
}
}
showFragment:
private void showFragment(String tag) {
// currentFragmentTag defines as FIRST_TAG in fields block
currentFragment = fm.findFragmentByTag(currentFragmentTag);
FragmentTransaction transaction = fm.beginTransaction();
if (currentFragment != null) {
transaction.hide(currentFragment);
}
transaction
.attach(getFragmentByTag(tag))
.show(getFragmentByTag(tag))
.commit();
currentFragmentTag = tag;
}
This was done to avoid load data in fragments every time when fragment is shown! All fragments added to FragmentManager only once and later there are only calls hide(previousFragment) and show(nextFragment).
This works well!
Before app goes to background and new heavy app is launched. In background activity of my app is destroyed and when I return to app there is only BottomNavigationView and empty display.
I checked in debug. And FragmentManaget.getActiveFragments() returns list of five fragments.
So, Activity is restored, FragmentManager is restored, FragmentManager contains fragment.
Why fragments not shown on display?
After skimming through your code what I can think of, is to set one of the five fragments as a default fragment to be displayed when the app runs.
Try something like:
bottomNavigationView.getMenu().findItem(R.id.first).setChecked(true);
Do this inside onCreate() or onResume();
Good luck!
I am trying to create a bottom navigation menu similar to WhatsApp, Viber etc. where I am switching between fragments. I got it somewhat working fragmentManager replacing the fragments, but as it turned out this re-creates the replaced fragments when I want to come back to one of the menu pages I have previously visited, changed etc.
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
Fragment fragment = null;
switch (item.getItemId()) {
case R.id.navigation_discover:
fragment = findFragment(RPageFragment.getTAG());
if(fragment == null){
fragment = RPageFragment.newInstance();
mFragmentManager.beginTransaction().add(R.id.fragment_container, fragment, fragment.getTag()).commit();
}
break;
case R.id.navigation_booked:
fragment = findFragment(MapViewFragment.getTAG());
if(fragment == null){
fragment = MapViewFragment.newInstance();
mFragmentManager.beginTransaction().add(R.id.fragment_container, fragment, fragment.getTag()).commit();
}
break;
case R.id.navigation_me:
fragment = findFragment(ProfilePageFragment.getTAG());
if(fragment == null){
fragment = ProfilePageFragment.newInstance();
mFragmentManager.beginTransaction().add(R.id.fragment_container, fragment, fragment.getTag()).commit();
}
break;
}
//Invalid menu option
if (fragment == null)
return false;
mFragmentManager.beginTransaction().replace(R.id.fragment_container,fragment).commit();
return true;
}
Is there any way to swap the fragments while preserving their view, state etc. similar to the way Whatsapp works ? (For example I have a mapView Fragment and I would like when I come back to it, to be showing the way I left it instead of recreating the mapView)
You can use How to implement a ViewPager with different Fragments / Layouts
You can simply check if fragment object null like below exaple:
case R.id.navigation_me:
fragment = findFragment(ProfilePageFragment.getTAG());
if (fragment == null) {
mFragmentManager.beginTransaction().show(fragment);
} else {
fragment = ProfilePageFragment.newInstance();
mFragmentManager.beginTransaction().add(R.id.fragment_container, fragment, fragment.getTag()).commit();
}
// and you can hide all other fragments here with
fragmentRPage = findFragment(RPageFragment.getTAG());
if (fragmentRPage == null) {
mFragmentManager.beginTransaction().hide(fragmentRPage);
}
fragmentMapView = findFragment(MapViewFragment.getTAG());
if (fragmentMapView == null) {
mFragmentManager.beginTransaction().hide(fragmentMapView);
}
//... and hide other fragments
break;
I have a bottom navigation bar with 4 menu items, I use FragmentTransaction show() , hide() methods to switch between fragments on tabs click , hide current fragment and show next fragment,
Before using show or hide method I must add fragment first and then use show or hide , until now everything work fine and fragment show with it's final state that's what I want.
But last added fragment with FragmentTransaction add() method is always active and it's view still clickable and switched fragment not clickable or touched.
I don't know why this happen .. any advice ?!!
This my code
private void changeFragment(Fragment fragment) {
if (fragment == null || fragment == currentFragment) return;
FragmentTransaction fragTransaction = getSupportFragmentManager().beginTransaction();
fragTransaction.setReorderingAllowed(true);
fragTransaction.setCustomAnimations(
R.anim.fade_in_transaction,
R.anim.fade_out_transaction
);
if (currentFragment != null && !currentFragment.isHidden()) {
fragTransaction.hide(currentFragment);
}
Fragment savedFragment = getSupportFragmentManager()
.findFragmentByTag(fragment.getClass().getSimpleName());
if (savedFragment == null) {
fragTransaction.add(R.id.container, fragment
, fragment.getClass().getSimpleName());
this.currentFragment = fragment;
} else {
if (savedFragment.isHidden()) {
fragTransaction.show(savedFragment);
}
this.currentFragment = savedFragment;
}
if (getSupportFragmentManager().getPrimaryNavigationFragment() == null) {
if (fragment instanceof FragmentHome) {
fragTransaction.setPrimaryNavigationFragment(fragment);
}
}
fragTransaction.commit();
}
I am adding a BottomNavigationView with three fragments to my app.
Everything works correctly, except one thing.
In the first fragment, there is an EditText view, in the second a ListView and in the third one some texts and images loaded from a JSON hosted in the server.
This is my code:
bottomNavigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.menu_dash:
fragment = new frg_dash();
break;
case R.id.menu_list:
fragment = new frg_list();
break;
case R.id.menu_info:
fragment = new frg_info();
break;
}
final FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.contenedor_principal, fragment).commit();
return true;
}
});
The problem is that every time I go from one fragment to another using the bottomNavigationView buttons, the Fragment starts all its execution again.
The result I'm looking for is that if the user in the second fragment is, for example, in the middle of the ListView, it goes to the third fragment and returns again, the ListView continues where it was.
Or if you press the button of the third Fragment in the bottomNavigationView, do not load the data again from the server.
I guess the problem is that when you click on a bottomNavigationView button, the fragment is created again:
... switch (id) {
case R.id.menu_dash:
fragment = new frg_dash();
break; ...
But it's just a guess. I suppose it can be controlled with the onCreate, onActivityCreated and onCreateView methods, but again, they are just my assumptions.
I've tried it with the hide () and show () parameters of the fragments, but without success ... or I'm not applying it well
I greatly appreciate the help in advance.
EDIT
This is my example currently with all the parts related to the answer:
public void replaceFragment(Fragment fragment, #Nullable Bundle bundle, boolean popBackStack, boolean findInStack) {
Log.v("2134", "Dentro");
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
String tag = fragment.getClass().getName();
Log.v("2134", "tag:" + tag);
Fragment parentFragment;
if (findInStack && fm.findFragmentByTag(tag) != null) {
parentFragment = fm.findFragmentByTag(tag);
} else {
parentFragment = fragment;
}
// if user passes the #bundle in not null, then can be added to the fragment
if (bundle != null) {
parentFragment.setArguments(bundle);
} else {
parentFragment.setArguments(null);
}
// this is for the very first fragment not to be added into the back stack.
if (popBackStack) {
fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
} else {
ft.addToBackStack(parentFragment.getClass().getName() + "");
}
ft.replace(R.id.contenedor_principal, parentFragment, tag);
ft.commit();
fm.executePendingTransactions();
}
...
bottomNavigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.menu_panel:
fragment = new frg_panel();
break;
case R.id.menu_promos:
fragment = new frg_promociones();
break;
case R.id.menu_catalogo:
fragment = new frg_catalogo();
break;
}
replaceFragment(fragment, null, true, true);
return true;
}
});
Use this code to open your fragment. Your fragment will not create every time. It will get same fragment from stack if exist.
/**
* replace or add fragment to the container
*
* #param fragment pass android.support.v4.app.Fragment
* #param bundle pass your extra bundle if any
* #param popBackStack if true it will clear back stack
* #param findInStack if true it will load old fragment if found
*/
public void replaceFragment(Fragment fragment, #Nullable Bundle bundle, boolean popBackStack, boolean findInStack) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
String tag = fragment.getClass().getName();
Fragment parentFragment;
if (findInStack && fm.findFragmentByTag(tag) != null) {
parentFragment = fm.findFragmentByTag(tag);
} else {
parentFragment = fragment;
}
// if user passes the #bundle in not null, then can be added to the fragment
if (bundle != null)
parentFragment.setArguments(bundle);
else parentFragment.setArguments(null);
// this is for the very first fragment not to be added into the back stack.
if (popBackStack) {
fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
} else {
ft.addToBackStack(parentFragment.getClass().getName() + "");
}
ft.replace(R.id.contenedor_principal, parentFragment, tag);
ft.commit();
fm.executePendingTransactions();
}
use it like
Update :
If your fragment is home or dashboard fragment then
Fragment f = new YourFragment();
replaceFragment(f, null, true, true);
Otherwise
Fragment f = new YourFragment();
replaceFragment(f, null, false, true);
Important This code is not replacement of saving all states or variables in fragment. This code will useful because it will not create fragment instance again.
For saving all states and variables in fragment for future use see this answer
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