after configuration change fragment from backstack is now sharing the FrameLayout?

ross studtman picture ross studtman · May 24, 2013 · Viewed 7.2k times · Source

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.

Answer

ross studtman picture ross studtman · May 25, 2013

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:
    1. 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.
    2. 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:

  1. Make a private Set of added fragments:

    // Collection of Frag Tags
    private Set<String> AddedFragmentTagsSet = new HashSet<String>();
    
  2. 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);
    }
    }
    
  3. 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.