First, some background information. I have an Activity that hosts several Fragments; these are hidden and shown such that only one Fragment is visible at a time. Each Fragment hosts several custom Views that I call IncrementCounters. Those views display a number and increment that number by 1 when tapped.
Each of the Fragments has setRetainInstance(true) called on it when it is created in my Activity. When the Activity is created, I check to see if the Fragments exist; if they do, I store a reference to them; if not, I create a new instance, like this:
autonFragment = (AutonomousScoutingFragment) getFragmentManager()
.findFragmentByTag("auton");
teleopFragment = (TeleoperatedScoutingFragment) getFragmentManager()
.findFragmentByTag("teleop");
postMatchFragment = (PostMatchScoutingFragment) getFragmentManager()
.findFragmentByTag("post_match");
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
if (autonFragment == null) {
Log.d("onCreate", "autonFragment is null!");
autonFragment = new AutonomousScoutingFragment();
autonFragment.setRetainInstance(true);
ft.add(R.id.scouting_fragment_container, autonFragment, "auton")
.hide(autonFragment);
}
if (teleopFragment == null) {
Log.d("onCreate", "teleopFragment is null!");
teleopFragment = new TeleoperatedScoutingFragment();
teleopFragment.setRetainInstance(true);
ft.add(R.id.scouting_fragment_container, teleopFragment, "teleop")
.hide(teleopFragment);
}
if (postMatchFragment == null) {
Log.d("onCreate", "postMatchFragment is null!");
postMatchFragment = new PostMatchScoutingFragment();
postMatchFragment.setRetainInstance(true);
ft.add(R.id.scouting_fragment_container, postMatchFragment,
"post_match").hide(postMatchFragment);
}
ft.commit();
One problem I have is that after every orientation change, it seems as though the Fragments aren't actually being retained, as ever time I see debug prints stating that they are all null. This may be realted to my bigger problem; I'm not sure.
I am trying to figure out how to maintain the value of the number stored in each IncrementCounter across configuration changes, specifically rotation. I have overridden onSaveInstanceState() and onRestoreInstanceState() in IncrementCounter. When I rotate my device, I see that onSaveInstanceState() is called on all of the IncrementCounters I have in my Fragments. However, the corresponding onRestoreInstanceState() is never called, and my IncrementCounters do not have their states restored. What is the proper way to handle something like this? I've been banging my head against my desk for hours about this problem.
Since the Fragments have the setRetainInstance(true), they will not be destroyed on a rotation. The Activity will be destroyed, and the Fragments will be detached until a new Activity is created. However, the Fragments will go through the onCreateView() method again, so you would need to restore their IncrementCounters.
Also, you can save state in onSaveInstanceState(), and then restore it in onCreate(), onCreateView(), and several other methods that are all passed that same bundle as a parameter.
Related
I am refactoring an Android App to use fragments. I have a fragment which I add to the layout with transaction.replace method but whose onCreateView method is not called. Code looks as follows:
FragmentManager fragmentManager = m_Activity.getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
m_StartNewGameFragment = new StartNewGameToggleButtonsFragment();
fragmentTransaction.replace(R.id.bottom_pane, m_StartNewGameFragment);
fragmentTransaction.commit();
String winnerText = null;
if (isComputerWinner)
winnerText = m_Activity.getResources().getText(R.string.computer_winner).toString();
else
winnerText = m_Activity.getResources().getText(R.string.player_winner).toString();
m_StartNewGameFragment.updateStats(computerWins, playerWins, winnerText);
The method updateStats of fragment is as follows:
public void updateStats(int computer_wins, int player_wins, String winnerText) {
System.out.println("Update stats " + m_ComputerWins);
m_ComputerWins.setText(Integer.toString(computer_wins));
m_PlayerWins.setText(Integer.toString(player_wins));
m_WinnerTextView.setText(winnerText);
}
When updateStats is called m_ComputerWins is null and the program crashes. m_ComputerWins is initialized inside the onCreateView method of the fragment which seems not to be called.
Can anyone please help ?
Take global variables in your fragment class for computer_wins, player_wins, winnerText and init them inside updateStats method.
then inside onViewCreated() method, set values like
m_ComputerWins.setText(Integer.toString(computer_wins));
m_PlayerWins.setText(Integer.toString(player_wins));
m_WinnerTextView.setText(winnerText);
You never know when the fragments is created (which calls onCreate()), because fragment's life cycle differs from your activity's. In your code, you are trying to update fragment from activity. But in the way you have written your code, it is almost guaranteed that your fragment is not created yet.
Search for how to pass parameters to fragment or update fragment to find out the correct way of updating fragment from activity. You can find several related question, e.g. this one.
FragmentTransaction only runs on the next event loop. If you want to immediately run a fragment transaction, use fragmentTransaction.commitNowAllowingStateLoss();.
Please note that this won't actually work correctly after process death, which is why you should use the fragment.setArguments(Bundle method to pass initial argument values to a fragment.
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.
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.
Problems with app:
When orientation changes the app is experiencing these problems:
Both FragmentA and FragmentC now occupy the FrameLayout container.
What works: Everything works as I want it to...prior to rotating the screen.
Activity description in brief:
EditActivity Purpose: edit collection and item fields.
Fragments this activity programmatically creates:
FragmentA - fragment for editing collection fields
FragmentB - ListFragment of items in collection
FragmentC - fragment for editing item fields.
Initial layout: FragmentA sits atop FragmentB, each in their own FrameLayouts.
When user clicks FragmentB's listview item: replace FragmentA with FragmentC to allow user to edit that item's fields. Now FragmentC sits atop FragmentB.
This seems like a very simple notion: the top portion of the activity is for editing either properties of the collection as a whole or a single item from the collection. I don't feel I have done anything wondrous with the layout so I'm a fair bit perplexed that a simple rotation of the phone (emulator) causes these problems that I am having such a dastardly time trying to fix.
Why the Android Fragment Guide example doesn't work for me: their example is much like what I am doing but their detail fragment is either being opened in a new activity or in its own Frame within the current activity, they don't do any swapping of fragments so I cannot glean how they would use the onSaveIstanceState to preserve the fragments that are visible and then use that information in onCreate to recreate the UI that was there prior to orientation change.
EDIT: took out one problem by caving and putting the listfragment in the XML, this solved the perpetual spinning "loading..." problem.
Solved. Oh, the rabbit holes I traveled... At any rate, if you run into problems like this a couple of things to consider:
ultimately I didn't have to write any code in onSaveInstanceState(Bundle outState).
Ultimately I didn't have to make any considerations about handling the backstack in onSaveInstanceState or deal with it the activity's onCreate.
When first "adding" fragments programmatically to the FrameLayout, use replace instead of `add' - this was likely one of the roots of my troubles.
in onCreate check if savedInstanceState's bundle is null, if(savedInstanceState == null), and if it is then I know that the activity hasn't been torn down previously by a configuration change, so here I build fragments that should be displayed right at activity start up. Other fragments that are programmatically brought to life elsewhere (ie, later than the activity's onCreate()), they don't belong in the if, they belong in the else:
else onSaveInstanceState != null and I know there's only one reason this thing's not null, because the system made a bundle named outState in onSaveInstanceState(Bundle outState) and hucked it at the activity's onCreate method where I can now get my grubbies on it. So it is here that I know a couple of things:
for sure the fragments I created in the activity's onCreate are still a part of the activity (I didn't detach or destroy them), but, I cannot make that same claim for the fragments brought to life via a user's actions, those fragments may or may not be currently (at the time of orientation aka configuration change) attached to the activity.
This is a good place for an if-this-thing-is-attached clause. One of things I initially messed up on was I failed to give ALL of my programmatically added fragments a tag; give all programmatically added fragments tags. I can then find out if the savedInstanceState bundle contains that key with savedInstanceState.containsKey(MY_FRAG_TAG) and with getFragmentManager().findFragmentByTag(MY_FRAG_TAG)
So here's the activity's onCreate (simplified):
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
// ...omitted code...
if(savedInstanceState == null){
// create fragment for collection edit buttons
editCollection = FragmentA.newInstance(someVariable);
// programmatically add fragment to ViewGroup
getFragmentManager().beginTransaction().replace(R.id.edit_topFrame, editCollection, EDIT_COLLECTIONS_TAG).commit();
}
// else there be stuff inside the savedInstanceState bundle
else{
// fragments that will always be in the savedInstanceState bundle
editCollectionFragment = (FragmentA)getFragmentManager().findFragmentByTag(EDIT_COLLECTIONS_TAG);
// fragments that may not be in the bundle
if(savedInstanceState.containsKey(EDIT_ITEM_TAG)){
editItemFragment = (FragmentC)getFragmentManager().getFragment(savedInstanceState, EDIT_ITEM_TAG);
}
}
// This fragment is NOT programmatically added, ie, it is statically found in an XML file.
// Hence, the system will take care of preserving this fragment on configuration changes.
listFrag = (ListViewFragment)getFragmentManager().findFragmentById(R.id.ListFragment);
// create adapter
adapter = new EditCursorAdapter(this, null);
// set list fragment adapter
listFrag.setListAdapter(adapter);
// prepare the loader
getLoaderManager().initLoader(LOADER_ID, null, this);
}
And the Activity's listener for the list fragment, where FragmentC is swapped for FragmentA:
// listfragment listener
#Override
public void listFragListener(Cursor cursor) {
// checking backstack size
Log.d(TAG, SCOPE +"backstack size: "+getFragmentManager().getBackStackEntryCount());
// With each listview click there should be only one item in the backstack.
getFragmentManager().popBackStack();
// create new fragment
editItemFragment = FragmentC.newInstance(cursor);
// programmatically add new fragment
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.edit_topFrame, editItemFragment, EDIT_ITEM_TAG);
ft.addToBackStack("pop all of these"); // was testing different ways of popping
ft.commit();
// interesting: this reports the same value as the first log in this method.
// ...clearly addToBackStack(null).commit() doesn't populate the backstack immediately?
Log.d(TAG, SCOPE +"backstack size: "+getFragmentManager().getBackStackEntryCount());
}
And onSaveInstanceState is naked as a jay bird:
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
Summary: I have the activity functioning exactly as I want it to.
Now, if I had a bunch of added fragments then I might handle them in a more programmatic fashion rather than by hard coding the if(savedInstanceState.contains(*hard coded key*). This I tested a little bit but cannot attest to its efficacy, however for someone out there this might spark an idea of what you can do:
Make a private Set of added fragments:
// Collection of Frag Tags
private Set<String> AddedFragmentTagsSet = new HashSet<String>();
In onAttachFragment do something like:
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
// logging which fragments get attached and when
Log.d(TAG, SCOPE +"attached fragment: " +fragment.toString());
// NOTE: XML frags have not frigg'n tags
// add attached fragment's tag to set of tags for attached fragments
AddedFragmentTagsSet.add(fragment.getTag());
// if a fragment has become detached remove its tag from the set
for(String tag : AddedFragmentTagsSet){
if(getFragmentManager().findFragmentByTag(tag).isDetached()){
AddedFragmentTagsSet.remove(tag);
}
Log.d(TAG, SCOPE +"contents of AddedFragmentTagsSet: " +tag);
}
}
Then in the activity's onCreate and within savedInstanceState clauses:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
// ...omitted code...
if(savedInstanceState == null){
// create fragment for collection edit buttons
editCollection = FragmentA.newInstance(someVariable);
// programmatically add fragment to ViewGroup
getFragmentManager().beginTransaction().replace(R.id.edit_topFrame, editCollection, EDIT_COLLECTIONS_TAG).commit();
}
// else there be stuff inside the savedInstanceState bundle
else{
// fragments that will always be in the savedInstanceState bundle
editCollectionFragment = (FragmentA)getFragmentManager().findFragmentByTag(EDIT_COLLECTIONS_TAG);
//////////// find entries that are common to AddedFragmentTagsSet & savedInstanceState's set of keys ///////////
Set<String> commonKeys = savedInstanceState.keySet();
commonKeys.retainAll(AddedFragmentTagsSet);
for(String key : commonKeys){
editItemFragment = FragmentC)getFragmentManager().getFragment(savedInstanceState, key);
}
}
}
...but that is untested and presented merely to spark ideas; in trying to figure out what was wrong with my activity's handling of configuration changes I did stumble and fumble in this direction and think it might bear fruit for the right person; though ultimately, obviously, I found a simpler way to fix my issues this time around.
I'll try to explain my problem:
all my fragments are using setRetainInstance(true)
In my activity onCreate I'm doing this:
if (savedInstanceState == null) {
fragment = onCreatePane();
fragment.setArguments(intentToFragmentArguments(getIntent()));
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.add(R.id.root_container, fragment, getFragmentTag());
trans.commit();
} else {
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
if ( fragment == null ) {
fragment = getSupportFragmentManager().findFragmentByTag(getFragmentTag());
}
if (fragment == null) {
fragment = onCreatePane();
fragment.setArguments(intentToFragmentArguments(getIntent()));
}
trans.add(R.id.root_container, fragment, getFragmentTag());
trans.commit();
}
So when I create the activity and savedInstance is null I create the Fragment, I set it's arguments, I begin the transaction and add my fragment to the transaction with it's own tag (to get it back later).
The user interact with the activity and change the orientation. The activity is destroyed and recreated (as normal activity lifelycle). So now it enter the else, the fragment is null and I do a fragment = getSupportFragmentManager().findFragmentByTag(getFragmentTag()); that returns the correct Fragment holded by the fragmentManager.
The problem is that this fragment hold a reference to the old Activity that has been destoyed so if I do a fragment.getActivity it returns null. How can I update the fragment reference to the acitivy to the new re-created Activity?
UPDATE: To be more precise I'm on the SearchActivity that call the onNewIntent when it get a new Search. So the actual interaction is this-> user do a search -> search is displayed correctly -> user change orientation, result is displayed correctly (if user interact with results they are fine) -> user do a new search from the search button and this call the SearchActivity's onNewIntent that dispatch the new intent to the fragment that has the search logic. Here it crashes because the reference to the activity is null
When and where are you calling getActivity()? The activity reference does get updated automatically, but not immediately. You should be safe to access it after onActivityCreated() was called.
just remove null check from else part if ( fragment == null ) { fragment = getSupportFragmentManager( ...... let update fragment with new one created on orientation change ......
If setRetainInstance(true) -- yes, it's the solution (to find fragment by tag, because instantiate it again -- is the wrong way -- there could be background routines in progress). Otherwise, if you have setRetainInstance(false) the problem can be back.
I had the similar one. I my case -- because I used Loader (in background). Solution was simple: to destroy fragment's loaders in onDestroy() method of the fragment.
In your baseAcvivity you can override onSaveInstanceStat to resolve this problem :
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//solution of fragment.getActivity() is null
outState.remove("android:support:fragments");
}