Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

findFragmentByTag null for Fragment A, if setRetain(true) on Fragment B

My problem involves an activity hosting three support fragments. One is a normal programmatic fragment (let's call it a home fragment). One is a portrait fragment added on top of the home fragment when the device is orientated, and one is 'headless', to continue an async task regardless of configuration changes. Very simple, I was working off this nice example.

public class HeadlessCustomerDetailFetchFragment extends Fragment{
private RequestCustomerDetails mRequest;
private AsyncFetchCustomerDetails mAsyncFetchCustomerDetails;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);

    mRequest = (RequestCustomerDetails)getActivity();
}

public void startFetching(String scannedBarcode) {
    if(mAsyncFetchCustomerDetails != null && mAsyncFetchCustomerDetails.getStatus() == AsyncTask.Status.RUNNING) return;

    if(mAsyncFetchCustomerDetails == null || mAsyncFetchCustomerDetails.getStatus() == AsyncTask.Status.FINISHED)
        mAsyncFetchCustomerDetails = new AsyncFetchCustomerDetails(getActivity(), mRequest, mPartner, scannedBarcode);
}

public void stopFetching() {
    if(mAsyncFetchCustomerDetails != null && mAsyncFetchCustomerDetails.getStatus() != AsyncTask.Status.RUNNING) return;
    mAsyncFetchCustomerDetails.cancel(true);
}

}

In my activity's onCreate() I create and add the headless fragment if necessary.

 mHeadlessCustomerDetailFetchFragment = (HeadlessCustomerDetailFetchFragment)getSupportFragmentManager()
            .findFragmentByTag(HeadlessCustomerDetailFetchFragment.class.getSimpleName());

if(mHeadlessCustomerDetailFetchFragment == null) {
         mHeadlessCustomerDetailFetchFragment = HeadlessCustomerDetailFetchFragment.instantiate(this, HeadlessCustomerDetailFetchFragment.class.getName());
    getSupportFragmentManager().beginTransaction()
            .add(mHeadlessCustomerDetailFetchFragment, mHeadlessCustomerDetailFetchFragment.getClass().getSimpleName())
            .commit();
    getSupportFragmentManager().executePendingTransactions();
        id = null;
    }

I then launch an async task (via my startFetching() function) after a 6 second delay (for testing) kicked off in the onCreateView() of the portrait fragment that is added when the orientation changes to portrait. The orientation change is detected in the activity's onCreate():

if (savedInstanceState == null) { 
   // Do some initial stuff for the home fragment
} 
else {
    getSupportFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
    if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
        //Launch portrait fragment
        FragmentLauncher.launchPortraitFragment(this);
    }

When the task is finished, I return to the activity and attempt to update the UI of the active portrait fragment, but the fragment manager cannot find it, findFragmentByTag() returns null.

To be clear:

  • The tag is correct
  • The fragment is found if I do not orientate the device, and instead kick off the async task somewhere else, during the activity's onResume() for example.
  • If I do not tell the headless fragment to retain itself - thereby losing the benefit of not recreating it, the portrait fragment is also correctly found.
  • Debugging I can see all 3 fragments in the manager if the headless one is not set to retain itself. If it is, I can only see the headless fragment.

Maybe retaining a fragment aggressively kills other fragments that are not retained or something to that effect?

like image 314
Daniel Wilson Avatar asked Oct 30 '15 22:10

Daniel Wilson


1 Answers

The root of the problem is how you maintain the reference to activity inside headless fragment.
It is not clear from the provided code how you update UI after completion of AsyncTask, lets assume you use mRequest from the first code snippet. You give mRequest to constructor when you need new AsyncTask and use this reference after AsyncTask completes.
It is ok, when you have no screen rotation between the moment when activity is created and when UI is updated. It is because you use the reference to activity which is still active.
It is not ok if you rotate screen. You have new activity every time after rotation. But mRequest is assigned only once when you create headless fragment in first call of activity’s onCreate(). So it contains reference to the first instance of activity which is not active after rotation. There are 2 instances of activity after rotation in your case: the first - which is referenced by mRequest and the second - which is visible and active. You can confirm this by logging the reference of activity inside onCreate: Log.i(TAG, "onCreate: this=" + this); and inside activity’s method which updates UI after async task: Log.i(TAG, "updating UI: this=" + this);
Besides the first activity is in Destroyed state. All fragments are detached from this activity and non retained fragments are destroyed. That’s why findFragmentByTag returns null.
If the headless fragment is not set to retain itself, then activity’s onCreate() recreates it in every call. So mRequest always references the last created activity with all fragments. In this case findFragmentByTag returns not null.

To avoid this problem I suggest:

  1. Use weak reference to store reference of Activity. Something like this:
    private WeakReference<RequestCustomerDetails> mRequest;
  2. Create a method in HeadlessCustomerDetailFetchFragment to update this reference.
    public void updateResultProcessor(RequestCustomerDetails requestCustomerDetails) { mRequest = new WeakReference(requestCustomerDetails); // Update ui if there is stored result of AsyncTask (see p.4b) }
  3. Call this method from activity's onCreate() every time.
  4. When AsyncTask finishes:
    a) if mRequest.get() is not null then update UI.
    b) if mRequest.get() is null then store result inside headless fragment and use it in p.2.

    Weak reference will allow GC to process destroyed activity and set null inside weak reference. Null inside weak reference will signal that there is no UI and nothing to update. Storing result of AsyncTask in headless fragment will allow to use this result for updating UI after its recreation.

    Hope this will help. Sorry for my English. If something is unclear I will try to explain.
like image 90
Nick S Avatar answered Sep 22 '22 00:09

Nick S