I am trying to explore about how fragments are managed by android framework and through my research I got to know so many new things that I didn't know about fragments but I got stuck at one point and can't figure out how this is happening.
Please try to understand my scenario first. It goes like that:
I have one Activity which add two fragments one by one. When activity is first loaded then Fragment A is attached to it using below code:
private void initFirstFragment(){
Bundle bundle = new Bundle();
bundle.putString("TEXT_TO_SHOW", "FIRST ACTIVITY\nFIRST DUMMY FRAGMENT");
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.frameLayoutFragmentContainer, FirstDummyFragment.newInstance(bundle), FirstDummyFragment.class.getSimpleName());
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
}
These callback methods of Fragment A is called when it is loaded
FirstDummyFragment: onCreate: savedInstanceState--->null
FirstDummyFragment: onCreateView: savedInstanceState--->null
FirstDummyFragment: onResume
Now in Fragment A, I have a edit text and I type some text into it.
When a button is clicked inside an Activity then Fragment B is added to same container using below code:
public void openSecondFragment() {
Bundle bundle = new Bundle();
bundle.putString("TEXT_TO_SHOW", "FIRST ACTIVITY\nSECOND DUMMY FRAGMENT");
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.frameLayoutFragmentContainer, SecondDummyFragment.newInstance(bundle), SecondDummyFragment.class.getSimpleName());
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
}
Below callback methods are called after adding Fragment B
SecondDummyFragment: onCreate: savedInstanceState--->null
FirstDummyFragment: onDestroyView
SecondDummyFragment: onCreateView
SecondDummyFragment: onResume
When I press back button, Fragment B is destroyed and Fragment A comes to foreground and below callback methods are called
SecondDummyFragment: onDestroyView
SecondDummyFragment: onDestroy
SecondDummyFragment: onDetach
FirstDummyFragment: onCreateView: savedInstanceState--->null
FirstDummyFragment: onResume
And the edit text of fragment A contains same text that I entered into it earlier before adding Fragment B. I am confused how android is restoring Fragment A's view state even if savedInstanceState is null and onCreateView returns a whole new View object when Fragment A is created again.
Finally I found my answer here.
Android is designed this way. View State Saving/Restoring are internally called inside Fragment in this case. As a result, every single View that is implemented a View State Saving/Restoring internally, for example EditText or TextView with android:freezeText="true", will be automatically saved and restored the state. Causes it to display just perfectly the same as previous.
Related
I am working on an application and there is one specific thing that is bothering me. Let's just say I have one activity and 2 fragments.FragmentA and FragmentB and FragmentA gets attached when activity starts.
I want to save the fragment data and fragment state when orientation changes occur.I have successfully saved fragment data using OnSavedInstanceState method. Now I want to save fragment state in the activity so that if orientation change occurs I want to be on the fragment I was (in my case either FragmentA or FragmentB depends on which was showing before config changes occur).
This is how I am saving the fragment state in the Activity:
#Override
protected void onSaveInstanceState(Bundle outState) {
// Save the values you need into "outState"
super.onSaveInstanceState(outState);
outState.putLong(SS_DATE, userDate.getTime());
android.support.v4.app.FragmentManager manager = getSupportFragmentManager();
Fragment currentFragment = this.getSupportFragmentManager().findFragmentById(R.id.content_container);
manager.putFragment(outState, "currentFragment", currentFragment);
}
And this is how I am retrieving on which fragment I was when the orientation change occurred:
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
FragmentManager manager = getSupportFragmentManager();
#SuppressLint("CommitTransaction")
FragmentTransaction transaction = manager.beginTransaction();
if (savedInstanceState != null) {
Fragment MyFragment = (Fragment) manager.getFragment(savedInstanceState, "currentFragment");
if (MyFragment instanceof FragListStudentsAttendance) {
Log.v("onRestore", FragListStudentsAttendance.TAG);
}else if (MyFragment instanceof FragGetClassesForAttendance){
Log.v("onRestore", FragGetClassesForAttendance.TAG);
if(MyFragment!=null) {
mFragGetClassesForAttendance = (FragGetClassesForAttendance) MyFragment;
}else{
mFragGetClassesForAttendance = new FragGetClassesForAttendance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// mFragGetClassesForAttendanceNew.setRetainInstance(true);
// transaction.replace(R.id.content_ssadmin_container, mFragGetClassesForAttendanceNew, "FragGetClassesForAttendance").addToBackStack(null);
transaction.add(R.id.content_ssadmin_container, mFragGetClassesForAttendance, FragGetClassesForAttendance.TAG);
//transaction.replace(R.id.newEnrollmentMainContainer, mFragNewEnrollmentResults).addToBackStack("FragNewEnrollments");
transaction.commit();
mFragGetClassesForAttendance.setDate(userDate);
}
}
}
}
Now
Scenario 1:
If I am on fragment A and I rotate the device every thing works fine as it should. Like fragment have web services which loads the data into listview so I check if data exist then there is no need to run the web service and that working for now
Scenario 2:
If I am on fragment B and orientation change occurs everything works fine as it is supposed to be on fragment B. Now When I press back button Fragment A gets called again and all the data also comes from service. I think this shouldn't happen because it was supposed to be in BackStack and it's data was saved. So what Should I do now here?
Scenario 3: On FragmentB I have noticed that when I rotates the device the saveInstanceState function of FragmentA also gets called. Why it is so? where as I was replacing the FragmentB with FragmentA ?
Some Confusions:
Let me talk about some of the confusions also , maybe someone clear it to me although I have searched and read a lot about fragment and activity life cycle,
Actually I want to save the data per activity and fragment on device rotation. I know how to do it with activity(how to save states) so I also know how to do it in the fragment (save state of fragment views) now I am confused how to tell activity which fragment was showing and which to go after config changes(rotation) ? also what happens to FragmentA if I am on FragmentB Does its get attach and detach again and again in background?
I got your problems and confusions. I think the life cycle of fragment is confusing you. and indeed it will confuse you.
You need to learn different situations.
1. Fragment Life cycle when it is in foreground (attaching and detaching with activity) . Please keenly observe all the methods that will call i.e OnSaveInstance,onCreateView,OnDestroyView,onDestroy
2. Fragment life cycle when it is in background (observe the methods stated above)
3. Fragment life cycle when it is added to backstack (and not in foreground)
I am quite sure you are confused with the point number 3. As when the fragment is added to backstack it never gets destroy. So rotating device twice will set the ffragment data to null. I think you are restoring data on ActivityCreated or on onViewCreated ,
Ill suggest you to restore the fragment data in the oncreate. this will work for you when your fragment is coming back to foreground from the backstack .
Example
private List<String> mCountries;</pre>
#Override
public void onCreate(Bundle savedInstanceState)
{
if (savedInstanceState != null)
{
// Populate countries from bundle
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_countries, container, false);
if (mCountries == null)
{
// Populate countries by calling AsyncTask
}
return view;
}
public void onSaveInstanceState(Bundle outState)
{
// Save countries into bundle
}
Hope this will clear your confusions.
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 have an Activity with a FrameLayout and need to show different fragments based on user input.
The code I use for showing a fragment is this:
private void showFragment(Fragment fragment, Bundle args, boolean addToBackStack) {
if (args != null) {
fragment.setArguments(args);
}
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.setCustomAnimations(R.anim.activity_open_translate, R.anim.activity_close_scale);
fragmentTransaction.replace(R.id.main_frame, fragment);
if (addToBackStack) {
fragmentTransaction.addToBackStack(fragment.getClass().getName());
}
fragmentTransaction.commit();
}
This is called as :
if (contactPickFragment == null) {
contactPickFragment = new ContactPickFragment();
}
showFragment(contactPickFragment, args, true);
All this works fine. Now if the user goes into one fragment presses back and returns back to the same fragment, all my views inside stay the same. For example, I have an EditText inside the fragment and the user edits something inside. If the user comes back to the fragment, the same text persists. I do not want this to happen. How do I reset everything in the view?
I have added code within the Fragment's onCreateView() to clear the text, and from debugging I see that this is being called, but the text never gets cleared. What am I doing wrong here?
If you don't want the data from the previous instance to appear, simply create a new instance of ContactPickFragment each time you show it.
Clearing data in onCreateView() has no effect because view state is restored AFTER onCreateView(). Your Fragment has no view before onCreateView() and so Android cannot possibly apply the previous state any earlier. Values set on the views during onCreateView() will be overwritten by their previous values.
As a general answer, there is no way to "refresh" the view of a Fragment, other than replacing the fragment with another instance of itself (possibly initialized with the parameters that you want to refresh/update).
You can reuse your fragments and refresh the state of your views. You just can't do it from onCreateView as #antonyt correctly points out.
Instead, override onViewStateRestored and set up the state of your views the way you'd like from there.
Something like:
#Override
public void onViewStateRestored(Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
View view = getView();
// Code to call view.findViewById to grab the views you want
// and set them to a specific state goes here
}
There are advantages to reusing fragments. Not the least of which is that if you have a memory leak with your fragment (which is easier than you may think to accomplish,) you will exacerbate the problem by creating myriads of them.
Orientation change from one fragment to another.
Orientation 1 (Landscape to Portrait):
onSaveInstanceState() of fragment 1.
onSaveInstanceState() of fragment 2.
onStop() of fragment 2.
onDestroy() of fragment 1.
onDetach() of fragment 1.
onAttach() of fragment 2.
onCreateView() of fragment 2.
onStart() of fragment 2.
Orientation 2 (Portrait to Landscape back):
onSaveInstanceState() of fragment 1.
onSaveInstanceState() of fragment 2.
onSaveInstanceState() of fragment 1.
onStop() of fragment 2.
onDestroy() of fragment 1.
onDetach() of fragment 1.
onDestroy() of fragment 1.
onDetach() of fragment 1.
onAttach() of fragment 2.
onCreateView() of fragment 2.
onStart() of fragment 2.
So as you notice that when I come back to fragment 1, the onSaveInstanceState(), onDestroy() and onDetach() are called two times for second orientation successive change.
Like that it keeps on increasing with every orientation change.
My activity code:
I am adding the fragment like this-
Fragment1 firstFragment = new Fragment1();
Bundle bundle = new Bundle();
firstFragment.setArguments(bundle);
getSupportFragmentManager().beginTransaction()
.replace(R.id.article_fragment, firstFragment)
.addToBackStack(null).commit();
UPDATE:
So what is happening is whenever I click on any tab, the addToBackStack adds the tabbed fragment into the container decreasing the memory and calling repeated life cycle methods.
Any way to effectively check if the fragment already exist then remove previous and add the current one ?
NOTE:
I tried --
if(savedInstanceState == null) { /*Add fragment*/ }
Fragment1 fragment = (Fragment1) getSupportFragmentManager().findFragmentById(R.id.article_fragment);
//if (fragment == null) { /*Add fragment*/ }
These aren't clean solutions causing other problems.
Probable cause to your Lifecycle might be, not handling Fragment BackStack correctly. That's why it might be calling multiple times. I too have faced this issues with fragments. But for now you can remove adding your Fragment to backStack to proceed.
You code would become:
Fragment1 firstFragment = new Fragment1();
Bundle bundle = new Bundle();
firstFragment.setArguments(bundle);
getSupportFragmentManager().beginTransaction()
.replace(R.id.article_fragment, firstFragment)
.commit();
Some examples where you can find handling AddingBackStack to a Fragment can be found at:http://www.javacodegeeks.com/2013/06/android-fragment-transaction-fragmentmanager-and-backstack.html
http://www.vogella.com/tutorials/AndroidFragments/article.html
And In order to retain the Fragments member values, you can use SharedPreference in Android to hold values and retrieve when activity is loaded.
http://developer.android.com/reference/android/content/SharedPreferences.html
Can I use savedInstanceState() to save the state when removing a fragment, then restore the state when I pop the fragment off the back stack? When I restore the fragment from the back stack, savedInstanceState bundle is always null.
Right now, the app flow is: fragment created -> fragment removed (added to back stack) -> fragment restored from back stack (savedInstanceState bundle is null).
Here is the relevant code:
public void onActivityCreated(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getArguments();
Long playlistId = bundle.getLong(Constants.PLAYLIST_ID);
int playlistItemId = bundle.getInt(Constants.PLAYLISTITEM_ID);
if (savedInstanceState == null) {
selectedVideoNumber = playlistItemId;
} else {
selectedVideoNumber = savedInstanceState.getInt("SELECTED_VIDEO");
}
}
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(Constants.SELECTED_VIDEO, selectedVideoNumber);
}
I think the problem is that onSavedInstanceState() is never called when being removed and being added to back stack. If I cant use onsavedInstanceState(), is there another way to fix this?
onSaveInstanceState is (unfortunately) not called in normal back-stack re-creation of a fragment. Check out http://developer.android.com/guide/components/fragments.html#Creating and the answer on How can I maintain fragment state when added to the back stack?
I like to store the View I return in onCreateView as a global variable and then when I return I simply check this:
if(mBaseView != null) {
// Remove the view from the parent
((ViewGroup)mBaseView.getParent()).removeView(mBaseView);
// Return it
return mBaseView;
}
The problem is that the fragment needs to have an Id or Tag associated with it in order for the FragmentManager to keep track of it.
There are at least 3 ways to do this:
In xml layout declare an Id for your fragment:
android:id=#+id/<Id>
If your fragments container View has an Id, use FragmentTransaction:
FragmentTransaction add (int containerViewId, Fragment fragment)
If your fragment is not associated with any View (e.g. headless fragment), give it a Tag:
FragmentTransaction add (Fragment fragment, String tag)
Also, see this SO answer.
FWIW, I hit this as well, but in my case onSaveInstanceState was called properly and I pushed in my state data when a new activity fragment was brought up on the smartphone. Same as you, the onActivityCreated was called w/ savedInstanceState always null. IMHO, I think it's a bug.
I worked around it by creating a static MyApplication state and putting the data there for the equivalent of "global variables"...