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;
Related
I'm using a bottom navigation view with four fragments, to avoid recreation of fragments I'm using this code snippet:
private void changeFragment(Fragment fragment, String tagFragmentName) {
FragmentTransaction ft = fm.beginTransaction();
Fragment currentFragment = fm.getPrimaryNavigationFragment();
if (currentFragment != null) {
ft.hide(currentFragment);
}
Fragment fragmentTemp = fm.findFragmentByTag(tagFragmentName);
if (fragmentTemp == null) {
fragmentTemp = fragment;
ft.add(R.id.content, fragmentTemp, tagFragmentName);
} else {
ft.show(fragmentTemp);
}
ft.setPrimaryNavigationFragment(fragmentTemp).commit();
}
private void showProspectFragment() {
changeFragment(ProspectContainerFragment.newInstance(), ProspectContainerFragment.class.getSimpleName());
}
private void showChatsFragment() {
changeFragment(ChatsFragment.newInstance(), ChatsFragment.class.getSimpleName());
}
....
// Bottom nav item click listener
binding.bottomNav.setOnNavigationItemSelectedListener(item -> {
switch (item.getItemId()) {
case R.id.prospect:
showProspectFragment();
return true;
case R.id.chat:
showChatsFragment();
break;
...
}
});
But whenever activity is recreated (using recreate()), for some reason fragments are not visible, no matter how many times I tap on bottom nav.
Are you using show/hide functionality? If so,
I think you must call one function in class onCreate method.
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!
How can I hide SearchFragment and display ChatFragment, without remove() in ViewPager? I had no issues with creating new instance everytime, but I don't want onDestroy() to have place, since i will be switching these 2 views very frequently. I want to create them once and have the ability to show/hide one for another.
My code is making two views to be seen in fragment_search(id cointainer)at once, then just crashes.
I was thinking about creating both Fragments at start, and then just manipulate them by attach and detach, but I can't really figure this out since ViewPager doesn't make it easier
public Fragment getItem(int position) {
switch (position) {
case 0:
if (fragment1 == null)
{
fragment1 = SearchFragment.newInstance(new SearchFragment.MyListener() {
#Override
public void onChatFound() {
// Dont want to remove fragment, just hide this view || fm.beginTransaction().remove(fragment1).commit();
Fragment chatTag = fm.findFragmentByTag("ChatTag");
if (chatTag == null)
{
chatFragment = ChatFragment.newInstance();
fm.beginTransaction().add(R.id.fragment_search, chatFragment, "ChatTag").commit();
}
fragment1 = chatTag;
notifyDataSetChanged();
}
});
}
return fragment1;
I have an activity which hosts three Fragment's and I can switch between Fragment's using BottomNavigationView.The mechanism that I use to switch between Fragment's is using show and hide functions of FragmentTransaction instead of replace function of FragmentTransaction. I am doing so because I want some network operations to be done only once and also to inflate Layout only once.
The problem that I am facing using this mechanism is that when I start another Activity from any of the Fragment and then hit the back button the selectedItem of the BottomNavigationView and the Fragment shown are mismatching.
I was able to solve this problem though but I feel it has less efficiency. The procedure was that whenever I clicked a tab in BottomNavigation while switching Fragment's I gave it some predecided number and saved in a static variable(X) and whenever I clicked back button in the OnResume() method of the hosting activity I made a switch-case block using X to know which Fragment was visible before starting the new Activity and then finally making three FragmentTransaction's to show and hide required Fragment's.
protected void onResume() {
super.onResume();
if(selectedId!=63){
switch(selectedId){
case 0:if(bottomNavigationView.getSelectedItemId()==R.id.navigation_home){handleHomeFragmentVisibility();}
break;
case 1:if(bottomNavigationView.getSelectedItemId()==R.id.navigation_dashboard)
{handleDashboardFragmentVisibility();}
break;
case 2:if(bottomNavigationView.getSelectedItemId()==R.id.navigation_notifications)
{handleNotificationFragmentVisibility();}
break;
}
}
I feel using three FragmentTransaction's is costly and I was looking for some efficient way. Can you tell me one if you know ?
public void handleHomeFragmentVisibility(){
FragmentManager fragmentManager= getSupportFragmentManager();
if (fragmentManager.findFragmentByTag("home") != null) {
//if the fragment exists, show it.
fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("home")).commit();
} else {
//if the fragment does not exist, add it to fragment manager.
Log.e(TAG,"homeFragmentAdded");
fragmentManager.beginTransaction().add(R.id.container, new HomeFragment(), "home").commit();
}
if (fragmentManager.findFragmentByTag("dashboard") != null) {
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("dashboard")).commit();
}
if (fragmentManager.findFragmentByTag("requests") != null) {
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("requests")).commit();
}
}
public void handleDashboardFragmentVisibility(){
FragmentManager fragmentManager= getSupportFragmentManager();
if (fragmentManager.findFragmentByTag("dashboard") != null) {
//if the fragment exists, show it.
fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("dashboard")).commit();
} else {
//if the fragment does not exist, add it to fragment manager.
fragmentManager.beginTransaction().add(R.id.container, new DashboardFragment(), "dashboard").commit();
}
if (fragmentManager.findFragmentByTag("home") != null) {
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("home")).commit();
}
if (fragmentManager.findFragmentByTag("requests") != null) {
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("requests")).commit();
}
}
public void handleNotificationFragmentVisibility(){
FragmentManager fragmentManager= getSupportFragmentManager();
if (fragmentManager.findFragmentByTag("requests") != null) {
//if the fragment exists, show it.
fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("requests")).commit();
} else {
//if the fragment does not exist, add it to fragment manager.
fragmentManager.beginTransaction().add(R.id.container, new NotificationFragment(), "requests").commit();
}
if (fragmentManager.findFragmentByTag("home") != null) {
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("home")).commit();
}
if (fragmentManager.findFragmentByTag("dashboard") != null) {
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("dashboard")).commit();
}
}
bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
FragmentManager fragmentManager = getSupportFragmentManager();
switch (item.getItemId()) {
case R.id.navigation_home:
selectedId=0;
handleHomeFragmentVisibility();
break;
case R.id.navigation_dashboard:
selectedId=1;
handleDashboardFragmentVisibility();
break;
case R.id.navigation_notifications:
selectedId=2;
handleNotificationFragmentVisibility();
break;
}
return true;
}
});
A first note on your code: Avoid boilerplate! Write only one method instead of three and use a signature of the type handleFragmentVisibility(String show, String hide1, String hide2, int container). In case the fragment to be shown is null, instantiate it by testing for show, something like:
Fragment newFragment = (show == "home") ? new HomeFragment() : (show == "dashboard") ? new DashboardFragment() : new NotificationFragment();
However, none of your fragments should ever get null through hiding (please check for yourself), since you don't remove them from your activity or replace them with other fragments. Instead of using show and hide you could also use attach and detach, both sets of methods keep state. I don't see an efficiency problem and you do indeed need to call three FragmentTransactions. It only can be done with less code:
public void handleFragmentVisibility(String show, String hide1, String hide2){
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag(show)).commit();
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag(hide1)).commit();
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag(hide2)).commit();
}
Please note, that although this method keeps the state of the fragment while hiding or detaching them, other events like orientation change still make it necessary that you take care of saving state in onSaveInstanceState(Bundle savedInstanceState).
I am using ActionBarSherlock and applying this pattern for the tab navigation that I found on android developer site. It's working pretty good but I also want to able to switch between NAVIGATION_MODE_TABS and NAVIGATION_MODE_LIST preserving the association between tabs and the fragments.
The pattern I mentioned above is pretty good for preserving a generic code. So I add listeners to my tabs and associate them with specific fragments like this:
bar.addTab(bar.newTab()
.setText("MyFragment")
.setTabListener(new TabListener<SomeFragment>(this, "myfargment", SomeFragment.class)));
and instantiate the fragment when the associated tab is clicked with the help of generics:
public void onTabSelected(Tab tab, FragmentTransaction ft) {
if (mFragment == null) {
mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
ft.add(android.R.id.content, mFragment, mTag);
} else {
ft.attach(mFragment);
}
}
My question is how can I achieve a similar way while navigating between my fragments with the list navigation mode. I couldn't find a similar way since the OnNavigationListener for the list on the ActionBar works for the whole list instead of per item basis like the tablistener.
or do I have to do something like this:
#Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
switch (itemPosition) {
case 0:
//Replace the current fragment with FragmentA
break;
case 1:
//Replace the current fragment with FragmentB
break;
case 2:
//Replace the current fragment with FragmentC
break;
default:
break;
}
return true;
}
EDIT:
I have noticed an interesting behaviour:
While the navigation mode is set to NAVIGATION_MODE_TABS if I put my phone in landscape mode it converts the tabs to a list and preservers the association between the fragments and the list items(which were tab items before) how can I achieve this result on demand rather than on orientation change?
I dont think its possible if you are in tab mode to manually set to a list navigation. I have the list navigation set for one of my applications when it falls below the "Large" bucket. This is how I am using the navigation listener:
OnNavigationListener mNavigationListener = new OnNavigationListener()
{
int mLastPosition = -1;
#Override
public boolean onNavigationItemSelected(int itemPosition, long itemId)
{
String selectedTag = mFragmentNames.get(itemPosition);
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentByTag(selectedTag);
FragmentTransaction ft = fm.beginTransaction();
/** Used to avoid the same fragment being reattached. */
if(mLastPosition != itemPosition)
{
/** Means there was a previous fragment attached. */
if(mLastPosition != -1)
{
Fragment lastFragment = fm.findFragmentByTag(mFragmentNames.get(mLastPosition));
if(lastFragment != null)
ft.remove(lastFragment);
}
if(fragment == null)
{
/** The fragment is being added for the first time. */
fragment = Fragment.instantiate(HomeActivity.this, selectedTag);
ft.add(R.id.rootLayout, fragment, selectedTag);
ft.commit();
} else
{
ft.attach(fragment);
ft.commit();
}
}
/**
* The newly attached fragment will be the last position if changed.
*/
mLastPosition = itemPosition;
return true;
}
};
The variable mFragmentNames is a HashMap that maps a integer position to a fragment name ex. "com.android.myproject.MyFragment"
I hope this helps.