I have an activity where I am calling three fragments - each depending on each other:
A(ctivity) -> f1 (Fragment one, title {is|should}: list) -> f2 (Fragment two, title {is|should}: overview) -> f3 (Fragment three, title {is|should}: detail)
ATM I use the following method call to jump backwards:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.getBackStackEntryCount()>0){
fragmentManager.popBackStack();
}
}
}
This works fine.
I am overriding the ActionBar title in each fragment like this:
ActionBar bar = getSherlockActivity().getSupportActionBar();
bar.setTitle(R.string.title_f3);
When navigating forward (like shown above) this works flawlessly but navigating backwards the title of the ActionBar isn´t updated:
f3 (title {is|should}: detail) -> f2 (title {is}: detail, {should}: overview) -> f1 (title {is}: detail, {should}: list)
Obviously I could just update it again when the fragment is shown. But my debugger never stops in any of the methods I´d except which would be called like onResume().
So is there actually any method being called in a previous fragment after popBackStack() ?
I know this is a bit late for an answer but for anyone who navigates here this might help!
First thing is first: popBackStack()doesn't pop a fragment, it pops a fragment transaction. So the last fragment transaction is reversed upon being called. If you were displaying FragmentA currently and your transaction was:
fragmentTransaction.replace(R.id.your_layout, fragmentB);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
It would replace FragmentA with FragmentB, and add that transaction (not the fragment) to the back stack. If you then hit the back button, it pops the transaction off the back stack, which was "replace this FragmentA with a FragmentB". Essentially, this instruction reverses the last transaction and removes it from the stack of transactions carried out. If the original FragmentA still exists, it uses that one. If it's been destroyed, it makes a new one.
So, if the Fragment hasn't been destroyed, then recalling the fragment after using on popBackStack(), the onStart() and onResume() methods are called. If the Fragment has been destroyed previously, then the lifecycle methods will be called starting from onAttach(). It's the same as pressing the back button on Activities.
Now the important bit, what happens re fragment lifecycle when we pop off back stack? Well as said before the fragment transaction is reversed so:
Scenario 1: Your fragmentB didn't already exist before transaction.
In this case the onCreate() and onAttach() methods are called during the transaction so the fragment will be destroyed and detached if you call popBackStack() and reverse the transaction (Note FragmentA probably already existed so replacing it wont destroy it as we're not undoing a fragment creation). In this case the lifecycle methods will be called starting from onAttach().
Scenario 2: Your fragmentB did already exist before transaction. In this case the fragment won't be destroyed and the next time you access it the onStart() and onResume() methods are called.
This fellow here explains a few things about using popbackstack() http://vinsol.com/blog/2014/09/19/transaction-backstack-and-its-management/ and the fragment lifecycle http://vinsol.com/blog/2014/10/22/fragment-view-state-retention-a-dirty-solution/. The other related posts are worth reading too!
use addOnBackStackChangedListener method in your BaseActivity, which will be called any time backstack changes
getSupportFragmentManager().addOnBackStackChangedListener(
new FragmentManager.OnBackStackChangedListener() {
public void onBackStackChanged() {
FragmentManager fm = getSupportFragmentManager();
if (fm != null) {
int backStackCount = fm.getBackStackEntryCount();
if (backStackCount == 0) {
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_menu);
}
setToolbarTittle(R.string.app_name);
} else {
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeAsUpIndicator(R.drawable.back);
}
}
}
}
});
My workaround is to get the current title of the actionbar in the Fragment before setting it to the new title. This way, once the Fragment is popped, I can change back to that title.
#Override
public void onResume() {
super.onResume();
// Get/Backup current title
mTitle = ((ActionBarActivity) getActivity()).getSupportActionBar()
.getTitle();
// Set new title
((ActionBarActivity) getActivity()).getSupportActionBar()
.setTitle(R.string.this_fragment_title);
}
#Override
public void onDestroy() {
// Set title back
((ActionBarActivity) getActivity()).getSupportActionBar()
.setTitle(mTitle);
super.onDestroy();
}
There's also one other good source that you can read through.
I think, the important point is the difference of the transaction you performed on Fragment B. If it's add, then no lifecycle methods are called on Fragment A, if it's replace, you will get some calls on those methods.
I use following workaround:
1) set 1st fragment setHasOptionsMenu(false) before add 2nd fragment on top of 1st one.
2) set 1st fragment setHasOptionsMenu(true) in onOptionsItemSelected() after return from 2nd fragment.
3) onCreateOptionsMenu() of 1st fragment should be called and you can change actionbar here.
But I want to know a better solution.
You can find a nice Fragment lifecycle diagram in the android docs here. So yes, if the fragment is brought to the foreground from the backstack onCreateView(), onActivityCreated(), onStart() and onResume() are called.
Related
In my app I have some menu items in a Navigation Drawer. When the user taps one of these items and navigates to the corresponding fragment, I want the fragment backstack to be cleared EXCEPT for the fragment they just navigated to. Let me illustrate:
In my nav drawer the user has options to open fragments F1, F2, F3, F4, F5, F6. When the app is first opened, the user starts on F1. Let's say from F1, the user opens fragments F7 -> F8 -> F9 -> F10 using buttons within the fragment views themselves. At this point, the backstack looks like:
F1 -> F7 -> F8 -> F9 -> F10
Suppose from here, the user opens the nav drawer and selects F2. What I want to happen is that the above backstack is cleared and is populated with ONLY F2, so that they user can't go back to F10 from F2. In otherwords, the backstack should become:
Not this: F1 -> F7 -> F8 -> F9 -> F10 -> F2
But this: F2
How can I accomplish this?
EDIT 1:
One of my attempted solutions was to add an OnNavigationItemSelectedListener to the navigationView when the activity is loading that would clear the backstack when one of the items was pressed. My solution worked, but in the process I overrode the default code that actually changes to the new fragment. I snooped through the Android API and found this:
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
boolean handled = onNavDestinationSelected(item, navController);
if (handled) {
ViewParent parent = navigationView.getParent();
if (parent instanceof DrawerLayout) {
((DrawerLayout) parent).closeDrawer(navigationView);
} else {
BottomSheetBehavior bottomSheetBehavior =
findBottomSheetBehavior(navigationView);
if (bottomSheetBehavior != null) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
}
return handled;
}
});
This is NOT my code. This is the native code that handles selection of menu items on the navigation drawer (found in the NavigationUI class). If I want to add my own OnNavigationItemSelectedListener I would have to recreate the functionality shown above, which I can't do since some of the methods in there are protected. So that's why I don't believe this solution will work.
You can try to clear fragments which are in backstack
FragmentManager manager = getSupportFragmentManager();
if (manager.getBackStackEntryCount() > 0) {
FragmentManager.BackStackEntry first = manager.getBackStackEntryAt(0);
manager.popBackStack(first.getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
I hope you are setting all this fragment in the same activity. If yes, then it will not be very hard to achieve whatever you want to achieve.
Make sure whenever you set your fragment in suppose MainActivity, follow this code.
val fragmentTransaction = supportFragmentManager.beginTransaction()
if (fragment == null) {
fragment = Frag2.newInstance()
fragmentTransaction.replace(R.id.container, fragment, Frag2.TAG)
} else {
fragmentTransaction.show(fragment)
}
fragmentTransaction.commit()
Main logic to achieve required behaviour is behind fragmentTransaction.replace().
So, whenever you set another fragment in a container, use replace() rather than add().
So, try this and give me feedback about it.
Happy coding..!
I have a profile settings fragment. When the user edits anything without saving and trying to switch to another fragment it should prevent it and show a dialog. I used to do that on activity by overriding onBackPressed method. In a fragment, I've used onDetach but it crashes when I put the super in the if statement.
#Override
public void onDetach() {
if (isProfileChanged) {
super.onDetach();
}
}
onDetach() is lifecycle method its Called when the fragment is no longer attached to its activity. So i.e its meant to get called once you replace or remove the current fragment .
Apart from it you want to restrict user from changing fragment. You should validate the fields and make a decision weather to change fragment or not.
Do this in Activity's onBackPressed(). below is an example illustrating the same for reference .
#Override
public void onBackPressed() {
Fragment fragment =getSupportFragmentManager().findFragmentById(R.id.containerFrame);
if (fragment != null && fragment instanceof YourFragment) {
if(((YourFragment) fragment).validate()){
finish();
}
} else {
super.onBackPressed();
}
}
Create a public method in fragment and validate the field and return true/false. Make the use of same method to transaction further .
public void onClick(View v) {
switch(v.getId())
{
case R.id.bAddYourNumber:
FragmentTransaction trans = getFragmentManager().beginTransaction();
trans.replace(R.id.root_frame, new InsertPastNumbersFragment());
trans.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
trans.addToBackStack(TAG);
trans.commit();
break;
}
When the button is pressed, the fragment is replaced and the previous one placed into back stack.
The problem is if the button is pressed several times, the same fragment is placed into backstack multiple times which results in the phone back button having to be pressed x amount of times the button was pressed to go back to the previous fragment.
Is there a way to control this to only add to back stack once?
Give your transaction a tag:
trans.replace(R.id.root_frame, new InsertPastNumbersFragment())
becomes
trans.replace(R.id.root_frame, new InsertPastNumbersFragment(), PAST_NUM_TAG)
Then, before handling the transaction, check:
if (getFragmentManager().findFragmentByTag(PAST_NUM_TAG) == null) {
// Fragment hasn't yet been added, do the transaction
} else {
// Fragment has already been added
}
Alternatively, if you have a button that triggers this, it might make more sense to just disable the button after you add the fragment to prevent the user from being able to press it more than once.
I have a frame layout in an activity to which i want to display different fragments inside. I have a sliding drawer with 3 options, each of which lead to a fragment being loaded inside the frame layout. Currently i use the following to accomplish this:
Fragment nextFragment = determineFragmentToSwitchTo(nextFragmentTag);
transaction.replace(R.id.fragment_container, nextFragment);
The first method determines what fragment i need by evaluating the nextFragmentTag string and loading a new fragment like so:
if (fragmentTag.equals(Constants.STUDENTPAGE))
nextFragment = new StudentFragment();
else if (fragmentTag.equals(Constants.TEACHERPAGE))
nextFragment = new TeacherFragment();
else if (fragmentTag.equals(Constants.PARENTPAGE))
nextFragment = new ParentFragment();
Clearly this approach is creating a new fragment each time and running through the whole fragment lifecycle without saving state. So if i am on the student page and scrolling through the student list and i switch to the parent page, when i go back to the student page, it reloads the entire list (i am fetching it from a server) and looses my place in it. How can i get it to persist state and sort of cache that fragment in the manager (if that makes sense)?
You could use the FragmentTransaction's hide(Fragment) and show(Fragment) methods, e.g.:
// In the parent Activity
StudentFragment studentFragment;
TeacherFragment teacherFragment;
ParentFragment parentFragment;
Fragment fragmentOnDisplay;
#Override
protected void onCreate(Bundle savedInstanceState) {
// Initialize fragmentManager, fragmentTransaction, etc.
studentFragment = (StudentFragment) fragmentManager.findFragmentByTag(Constants.STUDENTPAGE);
if (studentFragment == null) {
studentFragment = new StudentFragment ();
fragmentTransaction.add(R.id.your_frame_layout, studentFragment, Constants.STUDENTPAGE);
}
// repeat the same procedure for the other two fragments
// Suppose you want to begin with the teacherFragment on
// display - in that case hide the studentFragment and
// the parentFragment:
fragmentTransaction.hide(studentFragment);
fragmentTransaction.hide(parentFragment);
fragmentOnDisplay = teacherFragment;
fragmentTransaction.commit();
}
Now whenever you need to switch your fragments, simply hide the fragment on display, and show the fragment you need, e.g.:
...
fragmentTransaction.hide(fragmentOnDisplay);
fragmentTransaction.show(parentFragment);
fragmentOnDisplay = parentFragment;
fragmentTransaction.commit();
A navigation drawer with 3 Fragments, the third Fragment has a TextView with an on Click listener. Once it has been clicked a layout activity will open on the top which includes a ListView to allow the user to select/click on a specific Item, so later on this selected item info should be displayed on that TextView within the third fragment.
is there any method to pass data because I have used a class to pass data but the TextView wouldn't be refreshed with the sent data
This works as a design pattern to share arguments between the Activity and third fragment
--------------------------DataHolder Class---------------------------------
public class DataHolder {
private static DataHolder dataHolder = null;
private DataHolder() {
}
public static DataHolder getInstance() {
if (dataHolder == null)
{dataHolder = new DataHolder(); }
return dataHolder;
}
private String item;
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
If you find using startActivityForResult not sufficient in your case or using EventBus, you can overcome this by using SharedPreferences and Fragment/Activity lifecycle.
So once you start new Activity first Activity will go in onPause and with it all its Fragments will be put in onPause. When user clicks on one of the ListView items in your second Activity you can store the value inside SharedPreferences like:
PreferenceManager.getDefaultSharedPreferences(SecondActivity.this)
.edit().putString(key, selectedItemInfoHere).apply();
Then override inside your first Activity and in your third Fragment method onResume() and inside just make checking:
#Override
public void onResume() {
super.onResume();
String value = PreferenceManager.getDefaultSharedPreferences(getContext())
.getString(key, "");
if (value != null && !value.isEmpty()) {
//You have selected item value update TextView
}
}
Note that once you don't need this value you will need to remove it, because it will update your TextView every time when onResume is called. To remove value just call:
PreferenceManager.getDefaultSharedPreferences(getContext()).edit().remove(key);
If I understood you correctly, you have flow 3rd fragment --> activity which should update fragment which launched it. In this case, as for me, the most clean solution is from your opened activity call startActivityForResult method to call activity-host of your fragments and handle all what you need in overridden onActivityResult method. Than just call your fragment's updateTextView() or something like that.
On the other hand you can use this library to send messages between components, but you should be careful with usage and think about corner cases related to components lifecycle.
So, choose solution according your needs:)