4

I have an issue with Fragments and BroadcastManager. In my application I switched to one main activity and use the new NavigationDrawer. All content is contained in fragments.

One Fragment (Searching for users) contains two tabs (Search By Name, Search By Criteria) using the Navigation Pattern "Lateral Navigation" from the Android Design website: It has a ViewPager with a FragmentStatePagerAdapter.

|  Main Activity  |    |  Main Activity  |
-------------------    -------------------
| Search Fragment |    | Search Fragment |
| >Tab 1< | Tab 2 |    | Tab 1 | >Tab 2< |
|    View Pager   |    |    View Pager   |
|   Fragment 1    |    |   Fragment 2    |

I want both tabs to use the same action bar (options) menu: The search action. But only the active fragment should react to it.

I have tried different approaches, most annoying was that I cannot easily get the current fragment from the view pager directly (without relying on non-API implementation details).

Approach

I now use a LocalBroadcast to notify fragments that search has been clicked. Each fragment registers a small Wrapper-Receiver in onResume (and removes it in onPause) which forwards the onReceive to the fragment itself (the method shown below). I override setMenuVisibility which is a callback that the FragmentStatePagerAdpater calls on the Fragments to know which is the active fragment. Only the fragment that has the menu set visible will react to the broadcast.

ActionBar Tab -> ViewPager -> ViewPagerAdapter -> Fragment.setMenuVisibility
ActionBar Menu Action -> Broadcast ->  ... -> BroadcastReceiver -> Fragments

Both fragments trigger a fragment transaction to display the search results and add it to the back stack.

Issue The fragment transaction works in general, but when I rotate the device and then press search, I get an IllegalStateException (cannot commit after onSaveInstanceState).

@Override
    public void onReceive(Context context, Intent intent) {
        if (m_menuVisible) {
            final String s = ((TextView) getView().findViewById(R.id.search_text)).getText().toString();

            SearchResultsFragment f = new UserSearchResultsFragment();
            Bundle b = new Bundle();
            b.putString(SearchResultsFragment.EXTRA_SEARCHNAME, s);
            f.setArguments(b);

            FragmentTransaction transaction = getSherlockActivity().getSupportFragmentManager().beginTransaction();
            transaction.replace(R.id.fragment_container_frame, f).addToBackStack(null).commit(); // <<< CRASH
        }
    }

I tried to change the commit to commitAllowingStateLoss, but then I get an IllegalStateException with "Activity has been destroyed".

Do you know what goes wrong here? I am at a loss what to do...


Additional code:

MainActivities onCreate (based on NavigationDrawer sample) and selectItem

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
    setContentView(R.layout.fragment_container_layout);

    setupDrawer(); // Sets up the drawer layout, adapter, etc.

    if (savedInstanceState == null) {
        selectItem(0); // Selects and adds the fragment(s) for the position
    }

    // Other setup stuff
}

private void selectItem(int position) {
        // update the main content by replacing fragments

        Fragment f = null;
        switch (position) {
            case 0:
                f = ...
                break;
            case 1:
                ...
            default:
                throw new IllegalArgumentException("Could not find right fragment");
        }
        f.setRetainInstance(true);
        m_drawerList.setItemChecked(position, true);
        setTitle(mDrawerTitles.get(position).titleRes);

        // Hide any progress bar that might be visible in the actionbar
        setProgressBarIndeterminateVisibility(false);

        // When we select something from the navigation drawer, the back stack is discarded
        final FragmentManager fm = getSupportFragmentManager();
        fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

        fm.beginTransaction().replace(R.id.fragment_container_frame, f).commit();

        // update selected item and title, then close the drawer
        m_drawerLayout.closeDrawer(GravityCompat.START);
    }

The FragmentStatePagerAdapter in the malfunctioning tabbed fragment:

protected class TabPagerAdapter extends FragmentStatePagerAdapter {

        private List<String> m_fragmentTags = new ArrayList<String>();

        public TabPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        public void addFragments(List<String> list) {
            ViewPager pager = (ViewPager) getView().findViewById(R.id.viewpager);

            startUpdate(pager);
            for (String tag : list) {
                m_fragmentTags.add(tag);
            }
            finishUpdate(pager);
            notifyDataSetChanged();
        }

        public void addFragment(String tag) {
            addFragments(Collections.singletonList(tag));
        }

        @Override
        public Fragment getItem(int pos) {
            // CreateFragmentForTag: Retrieves the classes, instantiates the fragment
            // Does not do retainInstance or any transaction there.
            return createFragmentForTag(m_fragmentTags.get(pos));
        }

        @Override
        public int getCount() {
            return m_fragmentTags.size();
        }

}

A stripped down version of the project, containing only essentials, is posted at https://docs.google.com/file/d/0ByjUMh5UybW7cnB4a0NQeUlYM0E/edit?usp=sharing

Reproduce it as follows: Start up, you see the two tabs. Select either and click the Search in the ActionBar -> Fragment transaction works. Use Back key, rotate device and repeat -> Crash! [The file may not be accessible after the bounty has ended or error is identified.]

Edit: (1) Clarified that the onReceive lives in the context of the fragment (2) Added code for main activity

Patrick
  • 4,720
  • 4
  • 41
  • 71
  • We'd need more code to be able to answer this (for example, how are the fragments being added in the first place?). Also, can you make sure you're following [this pro-tip](https://plus.google.com/108967384991768947849/posts/3exHM3ZuCYM) on adding fragments? – curioustechizen Jun 25 '13 at 06:54
  • @curioustechizen I saw the pro-tip an instance ago in my stream too and checked. I used the navigation drawer sample as a base (http://developer.android.com/training/implementing-navigation/nav-drawer.html) and it does only create the main fragment if savedInstanceState is null. I'll try to add some code from the sub-fragments here... – Patrick Jun 25 '13 at 08:26
  • Ok, I added some code for the main activity which creates the fragments. The malfunctioning fragment basically only consists of the ViewPager containing the other fragments and the StatePagerAdapter. – Patrick Jun 25 '13 at 08:38
  • 1
    Hmm, I don't see anything immediately wrong. But since you see the crash only on rotation, could you see how it behaves with `setRetainInstance(false)` on your fragments? Also, it is still unclear from your code how `onReceive()` is ultimately called. My suspicion is a combination of `setRetainInstance(true)` and fragment rotation results in a case where the "wrong" `onReceive()` is called. – curioustechizen Jun 25 '13 at 09:19
  • The onReceive is called by a small wrapper (because we can't let it inherit from BroadcastReceiver). The fragment implements a one-method interface and registers the receiver with a reference to itself (registring/unregistring it on Resume/Pause). I'll try out the RetainInstance(false) and get back to you. – Patrick Jun 25 '13 at 10:37
  • Ah hell. You are correct. That fixed it. If you could convert the comment to an answer, I can accept it. Do you have an idea why the wrong fragment would be called, even if unregistering in onPause? – Patrick Jun 25 '13 at 17:47

2 Answers2

3

The reason for the error you get initially while using commit() is that a transaction has to be committed before any call of onSaveInstanceState() for the containing activity and that is why the error was rectified on using commitAllowingStateLoss (), which allows for possible loss of state after commit.

After this method change, you will be getting Activity has been destroyed exception during orientation changes because the fragment commit is holding reference to the original activity which got destroyed during orientation change. So instead of defining the fragment in your layout xml, you can define a fragment container and add your fragment to the container in the onCreate() of the parent activity to ensure that when activity is recreated, you are also creating a new fragment and commit

You can find details about setRetainInstance here.

Community
  • 1
  • 1
tony m
  • 4,769
  • 1
  • 21
  • 28
  • 1
    Yes, correct. Changing to `commitAllowingStateLoss` lead to such an exception. But I do not see what change you propose. Both the main activity and the tab-hosting adds the fragment dynamically, and not in the XML layout. Could you elaborate a bit on your proposed solution? – Patrick Jun 30 '13 at 12:01
2

Moving my comment into an answer here, and expanding on it.

The first clue is that you are crashing only after rotation, and you had setRetainInstance(true). First, try setting this to false. Even if that prevents the crash, you still need to be cautious. It could be pointing to some other bug that you are now obscuring by not retaining your fragment instance.

Is there a particular reason you had setRetainInstance(true) in the first place?

As to why you were seeing the crash to begin with, I still cannot pin-point the issue. My suspicion that the wrong onReceive is being called after rotation - but that's just a shot in the dark.

If you could create a skeleton of your app and post the code somewhere, I could look at it. I know you have already summarized how you "forward" the onReceive()s , but there is scope for introducing bugs there if not done right.

If this is not possible, here's what I suggest - put logging statements in the main life-cycle methods of your Activity, your Fragments and in your onReceive(). That would tell you the exact sequence of whats happening.

curioustechizen
  • 10,572
  • 10
  • 61
  • 110
  • I guess the reason was part laziness, part "it worked so far": The fragments use loaders to load data from web-apis and retaining them allowed to bypass the state saving with "onSaveInstanceState". I have added the lifecycle logging and it would see that with the `setRetainInstance(true)` the `onDestroy` method is never called as expected. But otherwise nothing that would jump out so far... I'll see if I can strip it down and post it somewhere. – Patrick Jun 27 '13 at 07:19
  • Right - the `onDestroy()` is never called, but `onPause()` is. So we still don't know why the wrong `onReceive()` was being called. In fact, I would stick my neck out here and say that the exception message you see (cannot commit after onSaveInstanceState) is probably misleading - the real issue might be something entirely different. Secondly, I would argue that using `setRetainInstance(true)` is the right thing to do for your use case since you don't want a rotation to disrupt loading data from web apis – curioustechizen Jun 27 '13 at 08:26
  • I have stripped down the project to the barebones and can reproduce it as follows: Start up, you see the two tabs. Select either and click the Search in the ActionBar -> Fragment transaction works. Use Back key, rotate device and repeat -> Crash! Source (Eclipse project) is located at the link below. You need ActionBarSherlock to compile it. https://docs.google.com/file/d/0ByjUMh5UybW7cnB4a0NQeUlYM0E/edit?usp=sharing – Patrick Jun 27 '13 at 12:23
  • I looked at your project and with a few changes and logging statement of my own, I determined that our suspicion was correct: When you do `setRetainInstance(true)`, `onSaveInstanceState()` is being called at some point when you perform the rotation; and after that we try to perform a `FragmentTransaction` like `replace`. When you `setRetainInstance(false)`, after `onSaveInstanceState()`, the fragment is destroyed and recreated - so technically you are performing a FragmentTransaction on a "fresh" fragment. – curioustechizen Jun 28 '13 at 06:39
  • 1
    As to why this might be a problem, I honestly cannot determine the exact reason. I've come across the same exception in the past (for a different use case that also involved Fragments and rotation). Searching through SO had led me to believe that if using fragments from the v4 support lib, one should be using `onResumeFragments()` instead of `onResume()`. I don't see how that would solve your problem though. – curioustechizen Jun 28 '13 at 06:41