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();
}
}
Related
So my current implementation is as follows. Basically I preload all the fragments, set them as fields of the current Activity, then I add or show if the fragment was already added.
mFeedTopicsFragment = FeedTopicsFragment.getInstance();
mUserDiscussionsFragment = UserDiscussionsFragment.getInstance(SessionPersistor.getSignedInUserId());
mUserConversationsFragment = MyConversationsFragment.getInstance(SessionPersistor.getSignedInUserId());
activityMainNavigation.setOnNavigationItemSelectedListener((MenuItem item) -> {
switch (item.getItemId()) {
case R.id.menu_item_go_to_feed_fragment:
if (getSupportFragmentManager().findFragmentByTag(FeedTopicsFragment.class.getSimpleName()) != null) {
getSupportFragmentManager()
.beginTransaction()
.show(mFeedTopicsFragment)
.hide(mUserDiscussionsFragment)
.hide(mUserConversationsFragment)
.commit();
} else {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.activity_main_container_fragment, mFeedTopicsFragment, FeedTopicsFragment.class.getSimpleName())
.hide(mUserDiscussionsFragment)
.hide(mUserConversationsFragment)
.commit();
}
return true;
case R.id.menu_item_go_to_user_discussions_fragment:
if (getSupportFragmentManager().findFragmentByTag(UserDiscussionsFragment.class.getSimpleName()) != null) {
getSupportFragmentManager()
.beginTransaction()
.show(mUserDiscussionsFragment)
.hide(mFeedTopicsFragment)
.hide(mUserConversationsFragment)
.commit();
} else {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.activity_main_container_fragment, mUserDiscussionsFragment, UserDiscussionsFragment.class.getSimpleName())
.hide(mFeedTopicsFragment)
.hide(mUserConversationsFragment)
.commit();
}
return true;
case R.id.menu_item_go_to_user_conversations_fragment:
if (getSupportFragmentManager().findFragmentByTag(MyConversationsFragment.class.getSimpleName()) != null) {
getSupportFragmentManager()
.beginTransaction()
.show(mUserConversationsFragment)
.hide(mFeedTopicsFragment)
.hide(mUserDiscussionsFragment)
.commit();
} else {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.activity_main_container_fragment, mUserConversationsFragment, MyConversationsFragment.class.getSimpleName())
.hide(mFeedTopicsFragment)
.hide(mUserDiscussionsFragment)
.commit();
}
return true;
However, I feel this is a bad approach since I'm wasting memory by preloading the fragments. However, I don't want to call replace each FragmentTransaction as that will call onCreateView each time and force a fresh API call each time I navigate between the fragments. I don't want this, as long as I remain in this Activity, say I start on FeedTopicsFragment, scroll down to the 10th item in the RecyclerView, navigate from FeedTopicsFragment to UserDiscussionsFragment, scroll down to the third item in the RecyclerView, and then back to FeedTopicsFragment, I will still see the 10th item, and back in UserDiscussionsFragment, I will see the third item.
I'm considering using onSaveInstanceState to save a Bundle of the data and load that Bundle in onActivityCreated. But I'm afraid onSaveInstanceState might not be called, neither will onActivityCreated. So what's the best solution for saving the state without creating an instance of the Fragment or saving it as a field and deleting that state once I navigate away from the Activity?
Each fragment currently has a loadData method that, inside that method, makes a call to the API and sets the RecyclerView, TextView, etc fields with the response data. Once I do a SwipeRefresh inside the Activity, it will call loadData on each of the Fragments.
I'm not sure how to do this refresh part either, basically I have to get an instance of all the Fragments and then call loadData on each of them. But only one fragment is in the activity_main_container_fragment at a time. In this case, I guess it makes sense to save an instance of each of the Fragments so then I can just call loadData on those local instances.
Basically you can check your Arraylist if null or empty before calling api.
if list has data then fill in listview locally,
else call api for getting data.
if you load fragment from stack then it will have its all variables like arraylist too.
First of all you should consider calling this method to replace your Fragment, because i saw you wrote too much code.
/**
* 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
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);
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
Is it possible to keep a Fragment running while I call replace from FragmentManager to open a new Fragment?
Basically I don't want to pause a Fragment when navigating (via replace method) to another Fragment.
Is it possible?
Or the correct approach is, always, instantiate a new Fragment every time I need to open it and restore its previous state?
Thanks!
FragmentManger replace method will destroy the previous fragment completely, So in each transaction onDestroyView(), onDestroy() and onDetach() will get called on previous fragment. If you want to keep your fragment running you can instead use FragmentManger hide() and show() methods! It hides and shows the fragments without destroying them.
so first add both fragments to fragment manager and also hide the second fragment.
fragmentManager.beginTransaction()
.add(R.id.new_card_container, FragmentA)
.add(R.id.new_card_container,FragmentB)
.hide(FragmentB)
.commit();
Note that you can only call show() on hidden fragment. So here you can't call show() on FragmentA but it's not a problem because by hiding and showing FragmentB you can get replacement effect you want.
And here is a method to go back and forth between your fragments.
public void showOtherFragment() {
if(FragmentB.isHidden()){
fragmentManager.beginTransaction()
.show(FragmentB).commit();
} else {
fragmentManager.beginTransaction()
.hide(FragmentB).commit();
}
}
Now if you put log message in fragment callback method you will see there is no destruction (except for screen orientation change!), even view will not get destroyed since onDistroyView doesn't get called.
There is only one problem and that is, first time when application starts onCreateView() method get called one time for each fragment (and it should be!) but when the orientation changes onCreateView() gets called twice for each fragment and that's because fragments once created as usual and once because of there attachment to FragmentManger (saved on bundle object) To avoid that you have two options 1) detach fragments in onSaveInstaneState() callback.
#Override
protected void onSaveInstanceState(Bundle outState) {
fragmentManager.beginTransaction()
.detach(FragmentA)
.detach(FragmentB)
.commit();
super.onSaveInstanceState(outState);
}
It's working but view state will not get updated automatically, for example if you have a EditText its text will erase each time orientation change happens. of course you can fix this simply by saving states in the fragment but you don't have to if you use the second option!
first i save a Boolean value in onSaveInstaneState() method to remember witch fragment is shown.
#Override
protected void onSaveInstanceState(Bundle outState) {
boolean isFragAVisible = true;
if(!FragmentB.isHidden())
isFragAVisible = false;
outState.putBoolean("isFragAVisible",isFragAVisible);
super.onSaveInstanceState(outState);
}
now in activity onCreate method i check to see if savedInstanceState == null. if yes do as usual if not activity is created for second time. so fragment manager already contains the fragments. So instead i'm getting a reference to my fragments from fragment manager. also i make sure correct fragment is shown since its not recovered automatically.
fragmentManager = getFragmentManager();
if(savedInstanceState == null){
FragmentA = new FragmentA();
FragmentB = new FragmentB();
fragmentManager.beginTransaction()
.add(R.id.new_card_container, FragmentA, "fragA")
.add(R.id.new_card_container, FragmentB, "fragB")
.hide(FragmentB)
.commit();
} else {
FragmentA = (FragmentA) fragmentManager.findFragmentByTag("fragA");
FragmentB = (FragmentB) fragmentManager.findFragmentByTag("fragB");
boolean isFragAVisible = savedInstanceState.getBoolean("isFragAVisible");
if(isFragAVisible)
fragmentManager.beginTransaction()
.hide(FragmentB)
.commit();
else
fragmentManager.beginTransaction()
.hide(FragmetA) //only if using transaction animation
.commit();
}
By now your fragment will work perfectly if are not using transaction animation. If you do, you also need to show and hide FragmentA. So when you want to show FragmentB first hide FragmentA then show FragmentB (in the same transaction) and when you want to hide FragmentB hide it first and also show FragmentA (again in the same transaction). Here is my code for card flip animation (downloaded from developer.goodle.com)
public void flipCard(String direction) {
int animationEnter, animationLeave;
if(direction == "left"){
animationEnter = R.animator.card_flip_right_in;
animationLeave = R.animator.card_flip_right_out;
} else {
animationEnter = R.animator.card_flip_left_in;
animationLeave = R.animator.card_flip_left_out;
}
if(cardBack.isHidden()){
fragmentManager.beginTransaction()
.setCustomAnimations(animationEnter, animationLeave)
.hide(cardFront)
.show(cardBack)
.commit();
} else {
fragmentManager.beginTransaction()
.setCustomAnimations(animationEnter,animationLeave)
.hide(cardBack)
.show(cardFront)
.commit();
}
}
I've five fragments in my activity. On the click of fragment drawer list, I'm calling setFrament() method.
It removes the previous fragments from the activity, and adds the new one as per the requirement.
Here's my code for setFragment method.
protected void setFragment(final int position) {
// Remove currently active fragment
if (mActiveFragment != null) {
Fragment previous;
while ((previous = mFragmentManager
.findFragmentByTag(mActiveFragment.toString())) != null) {
mFragmentManager.beginTransaction().remove(previous).commit();
Log.e("in loop", "stuck");
}
}
// It's enum, generated according to position
FragmentType type = getFragmentType(position);
Fragment fragment = mFragmentManager.findFragmentByTag(type.toString());
if (fragment == null) {
fragment = createFragment(type);
}
mFragmentManager
.beginTransaction()
.replace(R.id.containerFrameLayout, fragment,
type.toString()).commit();
// Sets the current selected fragment checked in
// Drawer listview
mFragmentDrawerList.setItemChecked(position, true);
// set Actionbar title
getActionBar().setTitle(type + "");
mActiveFragment = type;
}
Here, while changing the fragment, it keeps prining "stuck" in while loop forever,
my question is,
why remove(Fragment) method doesn't remove the previous fragment from the activity ?
commit() is an asynchronous call that is only executed when the Android system regains control. The fragments won't be removed until some time after you leave the method.
If you want to remove the previous fragment (assuming there's only one), then you can add them to the backstack. Then you can call popBackStack(null, 0) which will pop everything on the back stack. The side catch though is pressing the "back" button will also pop the backstack if the user were to do that. You'll have to override the onBackPressed() and handle it yourself if you don't want that to happen.
EDIT:
One method would be to keep track of all IDs or TAGs and call remove on them individually.
LinkedList<Integer> fragmentIds = new LinkedList<Integer>();
/*** Add fragment to FragmentManager ***/
fragmentIds.add(/** ID of fragment */);
/** Removing all fragments from FragmentManager **/
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragToRemove;
for(Integer id : fragmentIds)
{
fragToRemove = fm.findFragmentById(id);
transaction.remove(fragToRemove);
}
transaction.commit()
fragmentIds.clear();
However, you don't have to call remove on any of them so long as you use replace() method on the same container. replace() will pop the previous fragment and add in the new one. So long as the transaction isn't pushed to the backstack, the previous fragment is detached from the Activity and discarded.
Decided to rewrite this question:
I have three fragments call them A B C. Each has a view with some fields for the user to fill out. The user should be able to use a menu to switch between the different fragments. If the user fill outs information in fragment A and then switch to C fills out more information and then switches back to A the information the user typed in A should still be there.
I think I need to be using the FragmentManager somehow but I can't figure out the right combination of adds/ replaces / attaches ... that are required to make it work the way I want.
Can someone please provide a code snippet that would allow me to switch between fragments while maintaining each fragments view state.
Current Working Solution:
mContent is the active fragment and is a private member variable of the activity.
If anyone sees something wrong with this approach or a way to make it more efficient / more robust please let me know!
public void switchContent(String fragmentTag) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
if ( fragmentManager.findFragmentByTag( fragmentTag ) != mContent ) {
if ( !mContent.isDetached() ) {
transaction.detach( mContent );
}
if ( fragmentManager.findFragmentByTag( fragmentTag ) == null ) {
if ( fragmentTag.equals( "details" ) ) {
mContent = ScheduleDetailsFragment.newInstance();
} else if ( fragmentTag.equals( "notes" ) ) {
mContent = ScheduleNotesFragment.newInstance();
} else if ( fragmentTag.equals( "exceptions" ) ) {
// #TODO - Create Exceptions Fragment
}
} else {
mContent = ( SherlockFragment ) fragmentManager.findFragmentByTag( fragmentTag );
}
if ( mContent.isDetached() ) {
transaction.attach( mContent );
} else if ( !mContent.isAdded() ) {
transaction.add( R.id.content_frame, mContent, fragmentTag );
}
transaction.commit();
}
getSlidingMenu().showContent();
}
Thank You,
Nathan
I based my code of one of the examples provided but it returns a new fragment everytime a menu item is clicked so state information is lost.
If your pages hold onto some form of state/session, returning a new Fragment for every navigation event is a very poor user experience, and also not so good on memory usage.
I'd recommend having your sliding menu use the FragmentManager in your main Activity to maintain all of your Fragments that you switch between. It will manage all of their states, and you can retrieve them using findFragmentByTag(String tag). You would just need to ensure that each Fragment saves any state (values/data that changes after initial creation) that you add in onSaveInstanceState(Bundle outState) and restores it in onCreate(Bundle savedInstanceState). I'm not sure how the sliding menu you are using manages that currently, so this may not be possible. Hope this helps at least get you going in the right direction.
Edit: Updated answer for updated question -
Here is how I would switch the Fragments. By using replace, you will completely switch the Fragments, while reusing the already created ones. In order for your Fragments to saved/restore their state, you need to refer to my answer above. If that is unclear, search for some examples of how to save state in Fragments and Activities.
public void switchContent(String fragmentTag) {
// If our current fragment is null, or the new fragment is different, we need to change our current fragment
if (mContent == null || !mContent.getTag().equals(fragmentTag)) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
// Try to find the fragment we are switching to
Fragment fragment = fragmentManager.findFragmentByTag(fragmentTag);
// If the new fragment can't be found in the manager, create a new one
if (fragment == null) {
if (fragmentTag.equals("details")) {
mContent = ScheduleDetailsFragment.newInstance();
}
else if (fragmentTag.equals("notes")) {
mContent = ScheduleNotesFragment.newInstance();
}
else if (fragmentTag.equals("exceptions")) {
// #TODO - Create Exceptions Fragment
}
}
// Otherwise, we found our fragment in the manager, so we will reuse it
else {
mContent = (SherlockFragment) fragment;
}
// Replace our current fragment with the one we are changing to
transaction.replace(R.id.content_frame, mContent, fragmentTag);
transaction.commit();
getSlidingMenu().showContent();
}
else {
// Do nothing since we are already on the fragment being changed to
}
}