I have fragments A, B, C which I add with add() method.
When I reach fragment C, at some point I want to go back to fragment A and remove B and C.
My approach:
val backStateName = FragmentA::class.java.name
activity.fragmentManager.popBackStackImmediate(backStateName, FragmentManager.POP_BACK_STACK_INCLUSIVE)
I also have a specialTag added to my fragment A, so I did a check to make sure, that before I try my approach, fragment A is still in back stack.
val fragmentToGoTo = activity.fragmentManager.findFragmentByTag(specialTag)
and it doesn't return null - which means fragment is still available in back stack. popBackStackImmediate returns false. Why?
I had the same behaviour. Make sure that you call popBackStackImmediate on the same Thread as you used to add it to your backstack.
Also verify that you use .add() instead of .replace()
Anyway, it's never guaranteed that the backstack is not cleared/destroyed while doing this. I solved this behaviour by just using popBackStack() until you reach the fragment which you want to have.
You may try something like:
fun popStack(tag: String) {
var isPopped = fragmentManager.popBackStackImmediate(tag, FragmentManager.POP_BACK_STACK_INCLUSIVE)
if (!isPopped) {
fragmentManager.popBackStack()
//maybe a loop until you reached your goal.
}
}
When you attach a fragment (or perform any other action s.a. add/remove/detach etc.), you have an option to add it to the backstack with a name String:
FragmentA fragmentA = (FragmentA) fragmentManager.findFragmentByTag("A");
FragmentTransaction transaction = fragmentManager.beginTransaction();
if (fragmentA != null) {
transaction.attach(fragmentA);
transaction.addToBackStack("attachA");
transaction.commit();
}
Notice the "attachA" String we passed to the addToBackStack() method. We'll later use it to go back. Assume we've performed other transactions - added/removed/attached/detached some other fragments. Now to get back to the state we were call one of the popBackStack() methods:
fragmentManager.popBackStack("attachA", FragmentManager.POP_BACK_STACK_INCLUSIVE);
If there was a transaction added to the back stack with the name "attachA" - the method will take us back to that state.
Regarding your question about the return case - you've probably read the documentation about these methods and what values they return. I prefer to use the popBackStack() since it
/**
* Pop the last fragment transition from the manager's fragment
* back stack. If there is nothing to pop, false is returned.
* This function is asynchronous -- it enqueues the
* request to pop, but the action will not be performed until the application
* returns to its event loop.
*
* #param name If non-null, this is the name of a previous back state
* to look for; if found, all states up to that state will be popped. The
* {#link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether
* the named state itself is popped. If null, only the top state is popped.
* #param flags Either 0 or {#link #POP_BACK_STACK_INCLUSIVE}.
*/
public abstract void popBackStack(String name, int flags);
/**
* Like {#link #popBackStack(String, int)}, but performs the operation immediately
* inside of the call. This is like calling {#link #executePendingTransactions()}
* afterwards.
* #return Returns true if there was something popped, else false.
*/
public abstract boolean popBackStackImmediate(String name, int flags);
Related
I have a MainActivity and 4 fragments on it.
One of them is called ReportFragment and when the user reaches the last fragment (FinalFragment), it returns to the ReportFragment which is set as active by the fragmentManager.
Though, it is throwing an java.lang.IllegalStateException: Fragment already added and state has been saved when I put application on background and it returns to the ReportFragment.
It happens when I set arguments to the existing Fragment (ReportFragment).
Bundle arguments = newFragment.getArguments();
if (arguments == null) {
arguments = new Bundle();
}
arguments.putInt("CONTAINER", containerId);
newFragment.setArguments(arguments);
Why it does not happen when app is on foreground?
You cannot call setArguments() twice on a Fragment as written in it's java docs:
/**
* Supply the construction arguments for this fragment. This can only
* be called before the fragment has been attached to its activity; that
* is, you should call it immediately after constructing the fragment. The
* arguments supplied here will be retained across fragment destroy and
* creation.
*/
public void setArguments(Bundle args) {
if (mIndex >= 0) {
throw new IllegalStateException("Fragment already active");
}
mArguments = args;
}
Instead you can do the following to prevent the Exception:
if (newFragment.getArguments() == null) {
Bundle arguments = new Bundle();
arguments.putInt("CONTAINER", containerId);
} else {
newFragment.getArguments().putInt("CONTAINER", containerId);
}
In order to tell you why this happens when your app goes in background, it's is important to know when you call this peace of code. I assume you call it in a static newInstance method where you reference a static reference of your Fragment (newFragment).
So I have a fragment that is attached to an activity and I'm trying to make sure things go smoothly when the screen is rotated (or anything that would interrupt the activity). In order to do this, I use the methods onSaveInstanceState and onRestoreInstanceState in my activity to keep the information that my activity stores.
When the view for my fragment is created, the fragment asks the Activity for information (This is in onCreateView() for the fragment):
ArrayList<String> picList = mListener.getPics();
ArrayList<String> descripList = mListener.getDescriptions();
In order for the fragment to create the view, it needs access to picList and descripList, which are member variables for the activity. These member variables are stored and restored in onSaveInstanceState and onRestoreInstanceState.
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(photoFile != null)
outState.putString("photoFile", photoFile.getAbsolutePath());
outState.putString("currentFragTag", currentFragTag);
outState.putStringArrayList("picList", picList);
outState.putStringArrayList("descripList", descripList);
}
#Override
protected void onRestoreInstanceState(Bundle saved) {
super.onRestoreInstanceState(saved);
if(saved.getString("photoFile") != null)
photoFile = new File(saved.getString("photoFile"));
currentFragTag = saved.getString("currentFragTag");
picList = saved.getStringArrayList("picList");
descripList = saved.getStringArrayList("descripList");
currentFrag = getFragmentManager().findFragmentByTag(currentFragTag);
changeFrag(currentFrag, currentFragTag);
}
The problem is, onCreateView() is being called before onRestoreInstanceState() is being called in the activity. I tried using onActivityCreated() in the fragment instead, but that was also being called before onRestoreInstanceState(). With a debugger attached, when the screen is rotated, onRestoreInstanceState() is always called last. This means that the fragment does not have access to the activity's information when creating the view.
Is this supposed to happen? How can I have my fragment's view use information from the activity when the activity is being restored?
I think the most easiest way is using EventBus.
You can send a "msg" when your activity is recreated, and your fragment's "target method" will get this msg(the msg is Object, it can be a bundle).
Updated response:
Read the alternatives Passing data between a fragment and its container activity. Also see this.
Previous response revised:
See this and try to place your code in onResume() and invalidate the view or detach/attach the fragment as a quick solution but is not the best solution as Alex Lockwood said:
Fragments are re-usable UI components. They have their own lifecycle,
display their own view, and define their own behavior. You usually
don't need to have your Activity mess around with the internal
workings of a Fragment, as the Fragment's behavior should be
self-contained and independent of any particular Activity.
If you really need the code before, override the next methods and directly save/restore the required data in the fragment:
/**
* Called when the fragment's activity has been created and this
* fragment's view hierarchy instantiated. It can be used to do final
* initialization once these pieces are in place, such as retrieving
* views or restoring state. It is also useful for fragments that use
* {#link #setRetainInstance(boolean)} to retain their instance,
* as this callback tells the fragment when it is fully associated with
* the new activity instance. This is called after {#link #onCreateView}
* and before {#link #onViewStateRestored(Bundle)}.
*
* #param savedInstanceState If the fragment is being re-created from
* a previous saved state, this is the state.
*/
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
restoreInstanceState(savedInstanceState);
}
}
/**
* Called to ask the fragment to save its current dynamic state, so it
* can later be reconstructed in a new instance of its process is
* restarted. If a new instance of the fragment later needs to be
* created, the data you place in the Bundle here will be available
* in the Bundle given to {#link #onCreate(Bundle)},
* {#link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, and
* {#link #onActivityCreated(Bundle)}.
*
* <p>This corresponds to {#link Activity#onSaveInstanceState(Bundle)
* Activity.onSaveInstanceState(Bundle)} and most of the discussion there
* applies here as well. Note however: <em>this method may be called
* at any time before {#link #onDestroy()}</em>. There are many situations
* where a fragment may be mostly torn down (such as when placed on the
* back stack with no UI showing), but its state will not be saved until
* its owning activity actually needs to save its state.
*
* #param outState Bundle in which to place your saved state.
*/
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.put...;
}
And create this one, used to retrieve the needed data from bundle:
public void restoreInstanceState(Bundle savedInstanceState) {
... = savedInstanceState.get...
}
or use getActivity() method to directly access to some method or field from here if you need the code on your activity for some reason.
/**
* Return the {#link FragmentActivity} this fragment is currently associated with.
* May return {#code null} if the fragment is associated with a {#link Context}
* instead.
*/
final public FragmentActivity getActivity() {
return mHost == null ? null : (FragmentActivity) mHost.getActivity();
}
For example: ((YourActivity) getActivity()).getPics();
And add the getPics() method to the activity.
Further information here and an alternative solution defining an interface here.
I hold two Fragment instance in the activity ,add first fragment to activity , then replace second fragment to activity with setArgument() and addBackStack(), then press back button. now we return the first fragment , then we replace first to the second fragment which activity has hold once again , as the same with setArgument(), and it throws out a Exception ---- Fragment already active .
what's wrong with this process?
As per setArguments() source documentation, arguments supplied will be retained across fragment destroy and creation. So use getArguments() and then put bundle values to change the fields.
You can call it more than once or twice IF a Fragment is not attached to any Activity.
The code below is copied from Fragment.java
/**
* Supply the construction arguments for this fragment. This can only
* be called before the fragment has been attached to its activity; that
* is, you should call it immediately after constructing the fragment. The
* arguments supplied here will be retained across fragment destroy and
* creation.
*/
public void setArguments(Bundle args) {
if (mIndex >= 0) {
throw new IllegalStateException("Fragment already active");
}
mArguments = args;
}
You can call the method as long as you want IF not attached to the activity
Suppose I have 2 fragments A and B. A has 2 integer variables named data_1=2 and data_2=3. I do a transaction from Fragment A -> Fragment B. Note that Fragment B needs only data_1 but it doesn't need data_2, so, I send only the variable data_1 through Bundle. So, when I do another transaction from Fragment B -> Fragment A, sending back the modified value of data_1, I will use the new value of data_1 but will the original value of data_2 = 3 be retained ?
If not, then how do I retain this value?
There are various ways of doing it, but the easiest way in my Opinion would be to share data with the parent activity.
Basically like so:
class MainActivity extends Activity {
public static int data_1 = 1, data_2 = 2;
//All your other code goes here
}
Then in your child fragment to set the data you would go:
MainActivity.data_1 = 5;
Whereas to read the data you just call the static value.
int current_data = MainActivity.data_1;
If you need instances of the activity for whatever reason you can set up getter and setter functions for an instance's
(not static) variable.
you can also use onSavedInstanceState(Bundle outstate) callback method of fragment and set your value in the bundle. So when you come back to previous fragment, you will get the retain value from the bundle of the onCreate(Bundle savedInstanceState) callback method.
I have ActivityA attaching FragmentA. There's an EditText in FragmentA which, if focused, adds FragmentB (below). The stack trace starts with onDestroy in ActivityA, which triggers onFocusChange, which fires off popBackStack. The isRemovingOrPartOfRemovalChain() should be returning true at this point but it occasionally returns false causing the popBackStack, hence the exception. Is there a bug in that method?
editText.setOnFocusChangeListener(new OnFocusChangeListener(){
#Override
public void onFocusChange(View view, boolean hasFocus) {
if(hasFocus){
FragmentManager fragmentManager = getChildFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(FRAGMENT_B);
if(fragment == null){
FragmentB fragmentB = FragmentB.newInstance();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.fragment_b, fragmentB, FRAGMENT_B);
fragmentExploreSearchListTransaction.addToBackStack(null);
fragmentExploreSearchListTransaction.commit();
}
else{
if(!isRemovingOrPartOfRemovalChain()){
getChildFragmentManager().popBackStack();
}
}
}
});
public boolean isRemovingOrPartOfRemovalChain(){
if(isRemoving()){
return true;
}
Fragment fragment = this.getParentFragment();
if(fragment != null){
if(((MainFragment) fragment).isRemovingOrPartOfRemovalChain()){
return true;
}
else{
return false;
}
}
else{
return(getActivity().isFinishing());
}
}
/**
* Return true if this fragment is currently being removed from its
* activity. This is <em>not</em> whether its activity is finishing, but
* rather whether it is in the process of being removed from its activity.
*/
final public boolean isRemoving() {
return mRemoving;
}
When you commit fragment after onSavedInstanceState(Bundle outState) callback (e.g. onDestroy()), the committable Fragment state will be lost (becase Fragment.onSaveInstanceState(Bundle) won't be called in this situation).
In this case, when Activity is recreated, the committed fragment will not be present in Activity's FragmentManager, and so will not be restored. This is considered a state loss.
This behaviour might break or corrupt user experience, and is considered unintentional and exceptional, so the Android framework warns you about that by throwing an exception :-) Better save than sorry, right?
In case one knows what one's doing (which is almost always not so :-)), one may stick with .commitAllowingStateLoss(), but I strongly advise against it, as it will bring a legal bughole into your application.
Just do not commit fragments after it has became known that your Activity is destroying: for example,
...
boolean fieldActivityIsDestroying;
....
public void onSaveInstanceState(Bundle out){
super.onSaveInstanceState(out);
fieldActivityIsDestroying = true;
}
and check for field value when commiting the fragment.
Also you might want to FragmentManager.executePendingTransactions() to perform any fragment transactions immediately after you commit them to manager (default commit is asynchronous).
your issue because of activity state lost.
try this
fragmentExploreSearchListTransaction.commit()
to
fragmentExploreSearchListTransaction.commitAllowingStateLoss()
but it is not good solution, so i refer you read this blog, this blog is about fragment Transaction after save Activity Instance, I hope my information will help you.
There is not enough code to provide with the complete solution, but in this case:
It's better use Fragment#isRemoving() to check if fragment removed from activity;
If focus changed expected from user interaction with the screen it's better set/remove listener at following methods:
onCreateView()/onDestroyView();
onResume()/onPause();
If there is any reason that this solutions not work feel free to clarify.
Good luck!
Why don't you just set the onFocusedChangeListener in OnResume() and remove it in OnPause()?
That should prevent it from being triggered when your activity is finishing.