9

Currently, I would like to retain an expensive data structure, during configuration changes. I choose not to use Bundle to handle it, as the expensive data structure is not parcelable.

Hence, I use a non-UI Fragment (Called it RetainInstanceFragment), with its setRetainInstance(true) to hold the data structure.

public class RetainInstanceFragment extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Creating expensive data structure
        expensiveDataStructure = CreateExpensiveDataStructure();

        // Tell the framework to try to keep this fragment around
        // during a configuration change.
        setRetainInstance(true);
    }

    public ExpensiveDataStructure expensiveDataStructure = null;
}

An UI Fragment (Called it UIFragment) will get the expensive data structure from RetainInstanceFragment. Whenever there is configuration changes on UIFragment, UIFragment will always try to get the "cached" RetainInstanceFragment from FragmentManager, before it decides to create a new RetainInstanceFragment.

Example code is as follow.

public class UIFragment extends SherlockListFragment
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        FragmentManager fm = getFragmentManager();

        // Check to see if we have retained the worker fragment.
        retainInstanceFragment = (RetainInstanceFragment)fm.findFragmentByTag("data");

        // If not retained (or first time running), we need to create it.
        if (retainInstanceFragment == null) {
            retainInstanceFragment = new RetainInstanceFragment();
            fm.beginTransaction().add(watchlistArrayFragment, "data").commit();
        } else {
            // We can re-use retainInstanceFragment.expensiveDataStructure even
            // after configuration change.
        }
    }
}

However, there's a problem. Whenever I destroy my old UIFragment, and replace it with new UIFragment, I expect old RetainInstanceFragment will be destroyed as well. Here is how I destroy and create new UIFragment

public class MyFragmentActivity extends SlidingFragmentActivity    
    // Being triggered when there is different menu item in sliding menu being
    // selected.
    public void selectActiveContent(Country country) {
        Fragment fragment = new UIFragment(country);
        getSupportFragmentManager().beginTransaction().replace(R.id.content, fragment).commitAllowingStateLoss();
    }

But old RetainInstanceFragment is never destroyed.

My guess is, perhaps I forget to perform clean up in UIFragment. Hence, I add the following code

UIFragment

@Override
public void onDetach() {
    super.onDetach();
    // To differentiate whether this is a configuration changes, or we are
    // removing away this fragment?
    if (this.isRemoving()) {
        FragmentManager fm = getFragmentManager();
        fm.beginTransaction().remove(retainInstanceFragment).commit();
    }
}

However, it doesn't work all the time. I perform several sliding menu clicks.

1. selectActiveContent() -> Create new UIFragment and new RetainInstanceFragment
2. selectActiveContent() -> Create new UIFragment, but re-use previous RetainInstanceFragment. (Wrong behavior)
3. selectActiveContent() -> Create new UIFragment, and new RetainInstanceFragment.
4. selectActiveContent() -> Create new UIFragment, but re-use previous RetainInstanceFragment. (Wrong behavior)

Any idea how I can properly remove retained instance Fragment?

Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • Do you actually get in the `onDetach()` method? – user Mar 22 '13 at 08:27
  • Yes. I had perform logging onDetach. Everytime `selectActiveContent` is being called, the previous Fragment's onDetach will be triggered, with this.isRemoving() is true. – Cheok Yan Cheng Mar 22 '13 at 08:31
  • Did you tried removing the retained fragment in the `selectActiveContent()` method **before** the UI fragment's replace transaction? – user Mar 22 '13 at 08:44
  • @Luksprog Surprising it works! Do you have idea why your suggested method work but my previous method doesn't? I still prefer the previous, as required to call cleanup function explicitly doesn't look elegant :) Do you find any good official document which talks about this (Cleanup on retained fragment)? Sadly, I couldn't find one. – Cheok Yan Cheng Mar 22 '13 at 09:32
  • 2
    The fragment transactions are not made right away. My assumption was that doing that transaction in the `onDetach()` callback will not remove the retain fragment instance **before** the UI fragment's `replace` transaction finished and so your new UI fragment will still see the retain fragment instance still available, so it will not create a new one. Your previous method is not in the spirit of the fragments framework where fragments are unaware of other fragments and the activity manages all of them as it knows more about the overall application state. – user Mar 22 '13 at 09:47
  • 2
    The solution I proposed put the removing of the retained fragment instance **before** the replacing transaction of the UI fragments so the new UI fragment should see the retain fragment instance as unavailable. – user Mar 22 '13 at 09:53
  • ok. that makes sense. i hope i can give u credit, but i already answer the question by myself based on your suggestion :( – Cheok Yan Cheng Mar 22 '13 at 10:05
  • Just ended up here randomly from google. I am of the same opinion as Luksprog. Another way to solve the problem is to force the remove transaction in `onDetach()` with `FragmentManager.executePendingTransactions()`. – Gil Vegliach Mar 18 '16 at 21:50

2 Answers2

7

As suggested by @Luksprog, the following method works. However, it still do not explain why the previous cleanup done through onDetach doesn't work. If anyone can explain why this solution works and previous doesn't, I would be very thankful. :)

UIFragment

@Override
public void onDetach() {
    super.onDetach();
}

public void cleanupRetainInstanceFragment() {
    FragmentManager fm = getFragmentManager();
    fm.beginTransaction().remove(this.retainInstanceFragment).commit();
}

MyFragmentActivity

public class MyFragmentActivity extends SlidingFragmentActivity    
    // Being triggered when there is different menu item in sliding menu being
    // selected.
    public void selectActiveContent(Country country) {

        // *******************************************
        // Solution suggested by @Luksprog. It works!
        // But I have no idea why it works and previous doesn't work...
        // *******************************************
        Fragment oldFragment = getSupportFragmentManager().findFragmentById(R.id.content);
        if (oldFragment instanceof UIFragment) {
            ((UIFragment)oldFragment).cleanupRetainInstanceFragment();
        }

        Fragment fragment = new UIFragment(country);
        getSupportFragmentManager().beginTransaction().replace(R.id.content, fragment).commitAllowingStateLoss();
    }

(Edited) Useful comment by @Luksprog

The fragment transactions are not made right away. My assumption was that doing that transaction in the onDetach() callback will not remove the retain fragment instance before the UI fragment's replace transaction finished and so your new UI fragment will still see the retain fragment instance still available, so it will not create a new one. Your previous method is not in the spirit of the fragments framework where fragments are unaware of other fragments and the activity manages all of them as it knows more about the overall application state.

Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • I'm learning the usage of the fragment, as well i make some tests in a test project, the `replace` will call into the `removeFragment`, but it's different from calling 'removeFragment' explicitly. The **inactive** variable is true when calling from replace, which will cause the moveToState method can't go through the code which can make the old fragment is inactive and destroy. If you want see the code, link is here http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/app/FragmentManager.java#FragmentManagerImpl.removeFragment – dreamtale Sep 13 '13 at 15:59
  • Sorry, i made a mistake, just forget my comment :( – dreamtale Sep 13 '13 at 16:23
0

I think you can just remove the fragment from fragment transaction.

            if (mWorkFragment != null) {
              fm.beginTransaction().remove(mWorkFragment).commitAllowingStateLoss();
            }
Richard LIANG
  • 171
  • 2
  • 2