I have a navigation drawer and clicking on items shows/hides/creates full screen fragments.
For the most part, this code works great. But sometimes, maybe 1% of the time, I will get crazy full screen fragment overlapping when opening the app while it has already been running.
Is the problem with my code..? Or maybe something else in Android where it does not recognize I have the fragments with the tags already created?
Here is the relevant code for how I show/hide/create fragments:
#SuppressWarnings("StatementWithEmptyBody")
#Override
public boolean onNavigationItemSelected(MenuItem item) {
// Get to drawer layout so we can interact with it
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
// Get the fragment manager to remove/add fragments
FragmentManager fragmentManager = getSupportFragmentManager();
// Handle navigation view item clicks here.
int id = item.getItemId();
if (id == R.id.nav_profile) {
// Hide visible fragment
fragmentManager.beginTransaction().hide(getVisibleFragment()).commit();
// Check if the fragment exists first.
if(fragmentManager.findFragmentByTag("profileFragment") != null) {
// If the fragment exists, show it (no reason to recreate it).
fragmentManager.beginTransaction()
.show(fragmentManager.findFragmentByTag("profileFragment"))
.commit();
} else {
// If the fragment does not exist, add it to fragment manager with a tag to identify it.
// Create new fragment instance with required argument(s).
ProfileFragment fragment = ProfileFragment.newInstance();
fragmentManager.beginTransaction()
.add(R.id.content_frame, fragment, "profileFragment")
.commit();
}
// Set the title
mToolbarTitleTextView.setText(R.string.title_activity_profile);
} else if (id == R.id.nav_feed) {
// Hide visible fragment
fragmentManager.beginTransaction().hide(getVisibleFragment()).commit();
// Check if the fragment exists first.
if(fragmentManager.findFragmentByTag("feedFragment") != null) {
// If the fragment exists, show it (no reason to recreate it).
fragmentManager.beginTransaction()
.show(fragmentManager.findFragmentByTag("feedFragment"))
.commit();
} else {
// If the fragment does not exist, add it to fragment manager with a tag to identify it.
fragmentManager.beginTransaction()
.add(R.id.content_frame, new feedFragment(), "feedFragment")
.commit();
}
// Set the title
mToolbarTitleTextView.setText(R.string.title_activity_feed);
} else if (id == R.id.nav_notifications) {
// Hide visible fragment
fragmentManager.beginTransaction().hide(getVisibleFragment()).commit();
// Hide the post button
mPostButton.setVisibility(View.GONE);
// Check if the fragment exists first.
if(fragmentManager.findFragmentByTag("notificationsFragment") != null) {
// If the fragment exists, show it (no reason to recreate it).
fragmentManager.beginTransaction()
.show(fragmentManager.findFragmentByTag("notificationsFragment"))
.commit();
} else {
// If the fragment does not exist, add it to fragment manager with a tag to identify it.
fragmentManager.beginTransaction()
.add(R.id.content_frame, new NotificationsFragment(), "notificationsFragment")
.commit();
}
// Set the title
mToolbarTitleTextView.setText(R.string.title_activity_notifications);
}
mDrawerLayout.closeDrawer(GravityCompat.START);
return true;
}
// Useful method to hide the currently visible fragment
public Fragment getVisibleFragment(){
FragmentManager fragmentManager = MainActivity.this.getSupportFragmentManager();
List<Fragment> fragments = fragmentManager.getFragments();
if(fragments != null){
for(Fragment fragment : fragments){
if(fragment != null && fragment.isVisible())
return fragment;
}
}
return null;
}
EDIT: It is really hard to reproduce this error which makes it hard to debug. It seems to randomly happen.
Why hide and keep all the fragments with fragmentManager.beginTransaction().add(); you can avoid this error by keeping only one fragment in memory and avoiding the hassle of hiding fragments by using fragmentManager.beginTransaction().replace() and using the fragment lifecycle methods to store the fragment state if necessary.
Here is how I solved the problem. In my MainActivity I did this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
setContentView(R.layout.activity_main);
}
Basically what was happening is if I had 1+ fragments on the screen, if the android system ran low on resources while the app was in the background and shut it down, when restored, MainActivity.onCreate() would be called and it would re-instantiate all the fragments with the call
super.onCreate(savedInstanceState);
So I just made it null and this prevents from all those fragments to be recreated.
The reason they are overlapping is because they were all getting shown at once.
Definitely not the correct way to do it, but it solves my problem right now =P
Related
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 NavigationDrawer in my application and each menu item in drawer is a fragment.Whenever user chooses a menu item I replace the current fragment in the main container with the requested one but it recreates the fragment every-time, so i updated my code to reuse the existing fragments instead of creating them again and again as content of fragments remain same. My updated code to show fragment is :
public void showTabFragment() {
TabFragment Tf = (TabFragment) mFragmentManager.findFragmentByTag(Constants.TAB_FRAGMENT);
mFragmentTransaction = mFragmentManager.beginTransaction();
if (Tf != null) {
mFragmentTransaction.replace(R.id.containerView, Tf, Constants.TAB_FRAGMENT);
} else {
mFragmentTransaction.replace(R.id.containerView, new TabFragment(), Constants.TAB_FRAGMENT);
}
mFragmentTransaction.commit();
}
In above code I am trying to get fragments by Tag but it always returns null and executes the else case(new fragment).Could someone please guide me what am I doing wrong in my code?
I guess the code you've shown is for one of your menu fragment? If that's the case, what is probably happening is every time you open a menu item, the container is replaced with the new fragment(say, Fragment B) with its new tag(say, TAG 'B'). So, when you try to open the previous fragment(say, Fragment A) using it's tag(TAG 'A'), it won't be there, because that's what you replaced.
One possible solution is to hold references to the fragment as they are created, in, say a hashmap, and reuse them instead.
private HashMap<String, Fragment> menuFragments = new HashMap<>();
public void showMenu(String fragmentID)
{
MenuFragment fragment = menuFragments.get(fragmentID);
if(fragment == null)
{
fragment = new MenuFragment(); //Create the respective menu fragment based on the ID.
menuFragments.put(fragmentID, fragment);
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.containerView, fragment, fragmentID);
transaction.commit();
}
I use a mainActivity which contains a frameLayout.
In this framelayout i load different fragments (by clicking on a menuitem in a navigation drawer) When i switch between fragments with a static layout (.xml layout file) everything works fine. 1 fragment has a dynamic created layout (which is created in the onCreateView method). When i navigate to this fragment and navigate back to another fragment i can still see the "old" fragment with the dynamic created controls trough the "new" fragment.
The code what i use to switch between the fragment is:
mFragmentManager = getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
mFragmentTransaction.replace(R.id.fr_content_container,new AlertOverviewFragment()).commit();
mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
mDrawerLayout.closeDrawers();
if (menuItem.getItemId() == R.id.nav_item_alerts) {
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fr_content_container, new AlertOverviewFragment()).commit();
}
if (menuItem.getItemId() == R.id.nav_item_news) {
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fr_content_container, new NewsFragment()).commit();
}
if (menuItem.getItemId() == R.id.nav_item_add_country) {
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fr_content_container, new AddCountryFragment()).commit();
}
return false;
}
});
I can give the background of the new activity a background color so the old fragment isn't visible anymore but i think this is not the best practice solution. The old fragment will then still exist. What is the best way to solve this problem?
AND when i have this problem with this fragmen, are there then also other fragments that still exist in the background after navigate to another fragment? when yes, do i need to "destroy" these before switch to the new fragment or something?
I read something about the live cycles and understand this but i thought Android manages these states by him self?
I've written a method, setLoading in my activity that is responsible for setting the "loading" status of my app. This method is responsible for instantiating a LoadingFragment, removing any existing instances of it (Using FragmentManager) and then depending on it's first parameter loading, adding it to one of two possible containers (Depending on the top parameter).
protected LoadingFragment loadingFragment;
public void setLoading(boolean loading, boolean top) {
FragmentManager fragmentManager = getFragmentManager();
// Create LoadingFragment instance if it has not already been created
if (loadingFragment == null || !(loadingFragment instanceof LoadingFragment)) {
loadingFragment = new LoadingFragment();
}
// Remove the fragment first if it is present
fragmentManager
.beginTransaction()
.remove(loadingFragment)
.commit();
// Only if loading is true should we display the fragment
if (loading) {
// Decide which container we're going to put the fragment in
int id = top ? R.id.topContainer : R.id.container;
// Place the fragment in the right position
fragmentManager
.beginTransaction()
.add(id, loadingFragment)
.commit();
}
}
public void setLoading(boolean loading) {
setLoading(loading, true);
}
I am triggering setLoading(true) from elsewhere in my activity and I have commented out it's corresponding setLoading(false) while testing.
What I want to happen is for my LoadingFragment to appear every time setLoading(true) is called. The first call shouldn't remove anything since it at that point it doesn't exist. All subsequent calls should remove the existing LoadingFragment and add it again.
What happens is that the first call to setLoading(true) does indeed create the LoadingFragment and put it in the correct container. However, subsequent calls to setLoading(true) remove the fragment, but it never seems to be re-added. I have checked to see that the fragment does indeed exist and is of type LoadingFragment at the point it is added and I have also checked to ensure that it's onCreateView method is being called.
Am I doing something wrong?
Edit
Using the answer given below by H Raval as a base I have now come up with the following:
public void setLoading(boolean loading, boolean top) {
FragmentManager fragmentManager = getFragmentManager();
Fragment currentLoadingFragment = fragmentManager.findFragmentById(R.id.loadingFragment);
if (currentLoadingFragment != null) {
fragmentManager
.beginTransaction()
.remove(currentLoadingFragment)
.commit();
}
if (loading) {
int id = top ? R.id.topContainer : R.id.container;
fragmentManager
.beginTransaction()
.add(id, new LoadingFragment())
.commit();
}
}
This seems to work as expected. It seems that the primary difference is that this code is creating a new LoadingFragment instance each time (When loading = true) whereas originally I was trying to use the same instance and just adding/removing it using the FragmentManager.
Out of interest, is there a reason I need to create a new instance after using remove? Is this the correct way to do it? Or should it still work when using the same instance? Additionally, if it's recommended to create a new instance each time, is there anything I should do in terms of clean-up, freeing up resources etc. (Perhaps there's a way of gracefully destroying the obsolete instances)?
well i have made some changes in your code and works perfect for me..let me know if you face any difficulty
public void loadFragment(boolean loading, boolean top){
FragmentManager fragmentManager = getSupportFragmentManager();
loadingFragment = new LoadingFragment();
// Only if loading is true should we display the fragment
if (loading) {
// Decide which container we're going to put the fragment in
int id = top ? R.id.topContainer : R.id.container;
if(top){
if(fragmentManager.findFragmentByTag("loadingFragment")!=null)
fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("loadingFragment")).commit();
fragmentManager
.beginTransaction()
.replace(R.id.topContainer, loadingFragment,"toploadingFragment")
.commit();
}else{
if(fragmentManager.findFragmentByTag("toploadingFragment")!=null)
fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("toploadingFragment")).commit();
fragmentManager
.beginTransaction()
.replace(R.id.container, loadingFragment,"loadingFragment")
.commit();
}
}
I am building a navigation drawer as designed by the google documentation however I have an issue where the fragment is not being replaced. http://developer.android.com/training/implementing-navigation/nav-drawer.html
When the app first loads, the default fragment is loaded.
Clicking on another item on the drawer list leaves an empty view
However on rotating the device, loads the fragment chosen.
public void selectNavActivty(int position){
// TODO Changing between the different screens selection
fragment = null;
switch (position) {
case 0:
fragment = OverLay.newInstance();
break;
case 1:
fragment = Dummy.newInstance();
break;
}
if(fragment != null) {
// attach added to handle viewpager fragments
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.content_frame, fragment).attach(fragment)
.addToBackStack(null);
trans.commit();
getFragmentManager().executePendingTransactions();
} else {
Log.d("Drawer Activity","Error in creating Fragment");
}
}
For navigation menu fragment transactions I use the following approach, this way the fragment will be added and placed on top.
String name = "myFragment";
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.content_frame, fragment, name)
.commit();
Look up the attach() function. It follows a different fragment lifecycle.
Also make sure that your layout files framelayout is visible.
Modify your code as below:
if(fragment != null) {
// attach added to handle viewpager fragments
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.content_frame, fragment);
trans.addToBackStack(null);
trans.commit();
} else {
Log.d("Drawer Activity","Error in creating Fragment");
}
If the solution doesn't work for you, share the xml code along with your fragment code.
After adding Fragment it will be added to activity state and its view
will be added to defined Container view. But by attaching nothing will
be displayed if fragment was not already added to UI. It just attaches
to fragment manager. However if view was already added to a container
in UI and detached after that, by attaching it will be displayed again
in its container. Finally you can use attach and detach if you want to
destroy fragment View temporarily and want to display and build its
view on future without losing its state inside activity.
https://stackoverflow.com/a/18979024/3329488
My solution is to tag all the fragment with unique tag on fragment replacement. Make sure you also assign a unique tag to the default fragment during it creation. A more efficient way is to identify the fragment before you recreate the same one.
public void selectNavActivty(int position){
// TODO Changing between the different screens selection
FragmentManager fragmentManager = getSupportFragmentManager();
fragment = fragmentManager.findFragmentById(R.id.content_frame);
String fragmentTag = null;
switch (position) {
case 0:
fragmentTag = "case0Tag"; // please change to better tag name
break;
case 1:
fragmentTag = "case1Tag"; // please change to better tag name
break;
default:
Log.d("Drawer Activity","Error in creating Fragment");
return;
}
if (fragmentTag != null && !fragment.getTag().equals(fragmentTag))
fragmentManager.beginTransaction().replace(R.id.content_fragment, fragment, tag).commit();
}
In my case after rotating a device a blank fragment was shown. I understood that in an Activity.onCreate() I always called creating a blank Fragment and after that a needed one. So I changed it's behaviour to this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
openEmptyFragment()
openAnotherFragment()
}
}
I recommend to check savedInstanceState != null before adding new fragments, as written in Why won't Fragment retain state when screen is rotated?.