2

So basically what I'm working on is very similar to Instagram application, where there're a number of tabs and users can switch to any tab without any delay no matter what there's anything going on, such as refreshing, reloading, and etc. It also uses back button to go back to the previous preserved tab.

In order to achieve this, I've used FragmentManager with FragmentTransaction to show and hide each fragment which represents each tab. I didn't use replace or attach / detach because they destroy view hierarchy of previous tab.

My implementation works pretty well except that showing and hiding fragments are not committed (I highly doubt that this is a right word to say but so far that's how I understood the flow.), or don't occur immediately when SwipeRefreshLayout is refreshing on the fragment (to be hidden) which was added to FragmentManager later than the one to show.

My implementation follows the rules like these. Let's say we have 4 tabs and my MainActivity is showing the first tab, say FirstFragment, and the user selects the second tab, SecondFragment. Because SecondFragment had never been added before, I add it to FragmentManager by using FragmentTransaction.add and hide FirstFragment by using FragmentTransaction.hide. If the user selects the first tab again, because FirstFragment was previously added to FragmentManager, it doesn't add but only show FirstFragment and just hide SecondFragment. And selecting between these two tabs works smoothly.

But when the user "refreshes" SecondFragment's SwipeRefreshLayout and selects the first tab, FragmentTransaction waits for SecondFragment's refresh to be finished and commits(?) the actual transaction. The strange thing is that the transaction is committed immediately the other way around, from FirstFragment's refresh to SecondFragment.

Because this occurs by the order of addition to FragmentManager, I doubt that the order of addition somehow affects backstack of fragments and there might exists something like UI thread priority so that it forces the fragment transaction to be taken place after later-added fragment's UI transition finishes. But I just don't have enough clues to solve the issue. I've tried attach / detach and backstack thing on FragmentTransaction but couldn't solve the issue. I've tried both FragmentTransaction.commit and FragmentTransaction.commitAllowingStateLoss but neither solved the issue.

These are my MainActivity's sample code.

private ArrayList<Integer> mFragmentsStack; // This is simple psuedo-stack which only stores
                                            // the order of fragments stack to collaborate
                                            // when back button is pressed.
private ArrayList<Fragment> mFragmentsList;

@Override
protected void onCreate() {
    mFragmentsStack = new ArrayList<>();
    mFragmentsList = new ArrayList<>();
    mFragmentsList.add(FirstFragment.newInstance());
    mFragmentsList.add(SecondFragment.newInstance());
    mFragmentsList.add(ThirdFragment.newInstance());
    mFragmentsList.add(FourthFragment.newInstance());
    mMainTab = (MainTab) findViewById(R.id.main_tab);
    mMainTab.setOnMainTabClickListener(this);

    int currentTab = DEFAULT_TAB;

    mFragmentsStack.add(currentTab);
    getSupportFragmentManager().beginTransaction().add(R.id.main_frame_layout,
            mFragmentsList.get(currentTab), String.valueOf(currentTab)).commit();
    mMainTab.setCurrentTab(currentTab);
}

// This is custom interface.
@Override
public void onTabClick(int oldPosition, int newPosition) {
    if (oldPosition != newPosition) {
        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();

        // First hide the old tab.
        fragmentTransaction.hide(mFragmentsList.get(oldPosition));

        // Recalculate the fragment stack.
        if (mFragmentsStack.contains(newPosition)) {
            mFragmentsStack.remove((Integer) newPosition);
        }
        mFragmentsStack.add(newPosition);

        // Add new fragment if it's not added before, or show new fragment which was already hidden.
        Fragment fragment = getSupportFragmentManager().findFragmentByTag(String.valueOf(newPosition));
        if (fragment != null) {
            fragmentTransaction.show(fragment);
        } else {
            fragmentTransaction.add(R.id.main_frame_layout, mFragmentsList.get(newPosition),
                    String.valueOf(newPosition));
        }

        // Commit the transaction.
        fragmentTransaction.commitAllowingStateLoss();
    }
}

// It mimics the tab behavior of Instagram Android application.
@Override
public void onBackPressed() {
    // If there's only one fragment on stack, super.onBackPressed.
    // If it's not, then hide the current fragment and show the previous fragment.
    int lastIndexOfFragmentsStack = mFragmentsStack.size() - 1;
    if (lastIndexOfFragmentsStack - 1 >= 0) {
        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.hide(mFragmentsList.get(mFragmentsStack.get(lastIndexOfFragmentsStack)));
        fragmentTransaction.show(mFragmentsList.get(mFragmentsStack.get(lastIndexOfFragmentsStack - 1)));
        fragmentTransaction.commitAllowingStateLoss();
        mMainTab.setCurrentTab(mFragmentsStack.get(lastIndexOfFragmentsStack - 1));
        mFragmentsStack.remove(lastIndexOfFragmentsStack);
    } else {
        super.onBackPressed();
    }
}
Lee Han Kyeol
  • 2,371
  • 2
  • 29
  • 44
  • Have you tried calling executePendingTransactions()? Try that and see if it helps. – dora May 19 '15 at 03:57
  • @dora I added `executePendingTransactions()` after commit statements but it doesn't work. I'm wondering if it's a right place because what the document says seems to be the answer for my situation. – Lee Han Kyeol May 20 '15 at 01:49

1 Answers1

3

Just faced the same issue with only difference - I'm switching fragments on toolbar buttons click. I've managed to get rid of overlapping fragments overriding onHiddenChanged:

@Override
public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    if (hidden) {
        yourSwipeRefreshLayout.setRefreshing(false);
    }
}
  • I've thought about this, `setRefreshing` to false when changing tabs, but I didn't like the idea of forcing UI state to be different from what's actually going on. However I can't find any workaround so this would do the closest job. Thank you. – Lee Han Kyeol May 28 '15 at 02:42
  • Save my day!! I use infinite view pager with SwipeRefreshLayout. The refresh indicator will not show again if I keep scroll right to the same view second time => ex 0,1,2,0,1,2 . setRefreshing(false) in destroyItem() and it works!!! – JackWu Jan 13 '18 at 00:39