8

I'm working with the legacy code and I found an inconsistent behavior in this function:

@Override
public void openFragment(final Class<? extends BaseFragment> fragmentClass,
                         final boolean addToBackStack,
                         final Bundle args)
{
    long delay = 0;
    if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
        delay = getResources().getInteger(android.R.integer.config_shortAnimTime) * 2;
    }
    // FIXME: quick fix, but not all cases
    final Bundle args666 = args != null ? (Bundle) args.clone() : null;
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            doOpenFragment(fragmentClass, addToBackStack, args666);
        }
    }, delay);
    closeDrawer();
}


protected void doOpenFragment(final Class<? extends BaseFragment> fragmentClass,
                              final boolean addToBackStack,
                              final Bundle args)
{
    try {
        if (getSupportFragmentManager().getBackStackEntryCount() >= 1) {
            showNavigationIcon();
        }
        hideKeyboard();
        BaseFragment fragment = createFragment(fragmentClass, args);
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        fragment.initTransactionAnimation(transaction);
        String tag = getTag(fragment);
        transaction.add(R.id.container, fragment, tag);
        if (addToBackStack) {
            transaction.addToBackStack(tag);
        }
        transaction.commitAllowingStateLoss();
        hideLastFragment(0);
    } catch (Exception e) {
        Sentry.captureException(e, "Error opening fragment");
    }
}

openFragment gets non-empty Bundle args, but doOpenFragment will get empty Bundle. Fragments are committed by calling commitAllowingStateLoss()

A quick fix can be to use Bundle.clone():

    final Bundle args666 = (Bundle) args.clone();
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            doOpenFragment(fragmentClass, addToBackStack, args666);
        }
    }, delay);

It will not handle all cases and deepCopy is available only in api26.

  1. Why does it happen?
  2. How to fix it?

[UPDATE]

I played with @Pavel's solution and things get weirder

    final Bundle args666 = args != null ? cloneThroughSerialization(args) : args;
    final Bundle args777 = args != null ? (Bundle) args.clone() : args;

enter image description here

[UPDATE2]

Actually, the problem isn't with postDelayed call. Let's see the call stack:

enter image description here

in goRightToTheCollectionScreen the Bundle is created and packed (nothing suspicious, no mutation afterward).

I guess, the source of the problem in two calls inside openFragmentsChain:

public void openRootFragmentsChain(Class<? extends BaseFragment> fragmentClass,
                                   List<Class<? extends BaseFragment>> fragmentClasses,
                                   boolean addToBackStack,
                                   Bundle args)
{
    openFragmentsChain(fragmentClasses, addToBackStack, args);
    openFragment(fragmentClass, true, args);
}

public void openFragmentsChain(List<Class<? extends BaseFragment>> fragmentClasses,
                               boolean addToBackStack,
                               Bundle args)
{
    try {
        for (int i = 0; i < fragmentClasses.size(); i++) {
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            BaseFragment fragment = createFragment(fragmentClasses.get(i), args);
            String tag = getTag(fragment);
            transaction.add(R.id.container, fragment, tag);
            if (addToBackStack) {
                transaction.addToBackStack(tag);
            }
            if (i != fragmentClasses.size() - 1) {
                transaction.hide(fragment);
            }
            transaction.commitAllowingStateLoss();
        }
        if (fragmentClasses.size() >= 1) {
            updateDrawer();
        }
    } catch (Exception e) {
        Sentry.captureException(e, "Error opening fragment chain");
    }
}
protected void doOpenFragment(final Class<? extends BaseFragment> fragmentClass,
                              final boolean addToBackStack,
                              final Bundle args)
{
    try {
        if (getSupportFragmentManager().getBackStackEntryCount() >= 1) {
            showNavigationIcon();
        }
        hideKeyboard();
        BaseFragment fragment = createFragment(fragmentClass, args);
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        fragment.initTransactionAnimation(transaction);
        String tag = getTag(fragment);
        transaction.add(R.id.container, fragment, tag);
        if (addToBackStack) {
            transaction.addToBackStack(tag);
        }
        transaction.commitAllowingStateLoss();
        hideLastFragment(0);
    } catch (Exception e) {
        Sentry.captureException(e, "Error opening fragment");
    }
}

protected BaseFragment createFragment(Class<? extends BaseFragment> fragmentClass, Bundle args) throws Exception {
    BaseFragment fragment = fragmentClass.newInstance();
    fragment.setHasOptionsMenu(true);
    fragment.setArguments(args);
    fragment.setNavigationHandler(BaseFragmentNavigatorActivity.this);
    fragment.setToolbar(mToolbar);
    fragment.setMenuLoadService(mMenuLoaderService);
    return fragment;
}
Maxim G
  • 1,479
  • 1
  • 15
  • 23
  • What is the reason for the `Handler` madness? Post the code of the `doOpenFragment` method and an example call of the `openFragment` method. – artkoenig Aug 28 '17 at 13:20
  • It's used for waiting for an animation to finish. Updated question with some code. – Maxim G Aug 28 '17 at 15:20
  • Post the code of `createFragment`, please. This is not the right way to wait for the animation end, by the way. – artkoenig Aug 29 '17 at 07:45
  • Added function. How would you handle animation end? – Maxim G Aug 29 '17 at 08:07
  • I can't reproduce your problem. If you want to get it solved create a minimal example project and push it to github. Regarding the animation (I assume you mean the fragment transaction) have a look at [this answer](https://stackoverflow.com/questions/11120372/performing-action-after-fragment-transaction-animation-is-finished) – artkoenig Aug 29 '17 at 08:30
  • Just another feedback regarding your code, it reminds me on [that joke](http://www.smart-jokes.org/programmer-evolution.html). In my experience [KISS](https://en.wikipedia.org/wiki/KISS_principle) is the best thing to avoid bugs in general. – artkoenig Aug 29 '17 at 08:38
  • The joke is hilarious)))) especially Seasoned professional & Senior Manager. In free time I'm playing with the bug isolation (Activity + 2 fragments), no luck yet. – Maxim G Aug 30 '17 at 13:31

3 Answers3

3
  1. Some other code modifies the same Bundle before run() is called. Problem is in your code.
  2. You can deep clone through serialization.

        public static Bundle cloneThroughSerialization(@NonNull Bundle bundle) {
            Parcel parcel = Parcel.obtain();
            bundle.writeToParcel(parcel, 0);
    
            Bundle clonedBundle  = new Bundle();
            clonedBundle.readFromParcel(parcel);
    
            parcel.recycle();
            return clonedBundle;
        }
    
Pavel
  • 31
  • 3
  • 1. I don't get nullpointer or bundle with other values, just empty map what is strange. – Maxim G Aug 18 '17 at 16:02
  • 2. parcel doesn't always do deep copy https://stackoverflow.com/questions/39916021/does-retrieving-a-parcelable-object-through-bundle-always-create-new-copy – Maxim G Aug 18 '17 at 16:03
2

The code you posted seems ok, so probably your bundle is modified elsewhere (even though your postDelayed delay is 0, the runnable will be executed slightly later and its possible you modify the bundle meanwhile). Try to execute it directly without postDelayed, to see if the problem still persists. You can post more of your code, maybe we can figure out where else you touch that bundle.

If nothing else helps, you can always copy the method from API26 to your code and use it (edge case - this seems a simple issue so you shouldn't have to)

Nick
  • 585
  • 4
  • 11
  • @MaximG it appears they haven't released it yet, you can implement a deep copy manually(or just serialize-deserialize), which I wouldn't do or recommend. have you tried without the postDelayed? – Nick Aug 28 '17 at 16:24
0

Could not model bundle args going to "empty". Made simple code with the fragment calling Handler example with with the specified args value. If i modelling something wrong pls give me hint

public class MainActivity extends FragmentActivity {

private  static final String LOG_TAG = "Main activity";

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.news_articles)

        // Create an instance of ExampleFragment
        final HeadlinesFragment firstFragment = new HeadlinesFragment();

        // Add the fragment to the 'fragment_container' FrameLayout
        getSupportFragmentManager().beginTransaction()
                .add(R.id.fragment_container, firstFragment).commit();

        long delay = 0;
        final boolean addToBackStack = true;
        final Bundle args666 = new Bundle();
        args666.putInt("hi", 666);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                doOpenFragment(firstFragment, addToBackStack, args666);
            }
        }, delay);
    }

protected void doOpenFragment(HeadlinesFragment firstFragment,
                              final boolean addToBackStack,
                              final Bundle args){
    int value = args.getInt("hi");
    Log.d(LOG_TAG, "The value is " + value);
}
}
Leontsev Anton
  • 727
  • 7
  • 12