avatarElye - A One Eye Dev By His Grace

Summary

The article discusses a peculiar issue encountered when using a combination of add and replace methods for fragment transactions in an Android application, which causes the toolbar's back navigation to malfunction.

Abstract

The author of the article shares their experience with a challenging bug related to fragment stack management in Android development. The issue arises when mixing add and replace fragment transactions, leading to a scenario where the toolbar's back button stops responding. This problem is particularly evident when the user attempts to pop fragments off the back stack. The author provides a detailed analysis of the problem, including the steps to reproduce it, and proposes a workaround while an official fix from Google is pending. The article also includes a puzzle for readers to deduce the pattern that triggers the issue and links to the source code and a Stack Overflow discussion for further investigation.

Opinions

  • The author believes that using replace is generally preferable over add when managing fragments to avoid unnecessary memory usage and reduce UI rendering needs, except when dealing with fragments containing a webview.
  • The author suspects that the issue is a bug within the Android SDK library, specifically with the setSupportActionBar method, and has filed a report with Google.
  • The workaround suggested by the author involves resetting the toolbar for the top-level fragment during back stack changes to ensure consistent navigation behavior.
  • The author expresses a desire for a more ideal solution from the Android community or an official fix from Google, indicating a level of dissatisfaction with the current state of the workaround.
  • By sharing the problem and the workaround, the author aims to help other developers who might encounter the same issue, demonstrating a collaborative and proactive approach to problem-solving within the developer community.

Puzzle: Fragment stack pop cause issue on toolbar

Using fragment, with toolbar is so commonly done these days, and I don’t expect to find any issue with it, until this week. I faced this seemingly weird issue and uncovering the cause of it took me about 2 days. Sharing this so you could learn about it early if you ever see such behavior.

Also I have a related puzzle below for you to solve 😉.

Background

I have an app, with a layout container in the activity. The container took up the entire width of the activity, and it is inflated with fragment or fragments.

Toolbar setup in each fragment

In my app, each of the fragment would have their respective toolbar with it’s menu. As there’s no common toolbar, it is set in the fragment instead of the activity. The code as below

(activity as AppCompatActivity).setSupportActionBar(toolbar)

In all my toolbar, I have menu that enable one to click and

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    if (item.itemId == android.R.id.home) {
        activity?.onBackPressed()
        return true
    } else {
        // Do some other things to other menu
        return super.onOptionsItemSelected(item)
    }
}

On my activity, my back-press is to pop the fragment

override fun onBackPressed() {
    if (supportFragmentManager.backStackEntryCount > 0) {
        supportFragmentManager.popBackStackImmediate()
    } else {
        super.onBackPressed()
    }
}

Mixture of add and replace fragment usage

Besides, for some cases I use replace to add my fragment to the stack, and sometimes I use add to add my fragment to the stack.

private fun addFragment(fragment: Fragment, name: String) {
    supportFragmentManager.beginTransaction()
            .add(R.id.container, fragment, name)
            .addToBackStack(name).commit()
}

private fun replaceFragment(fragment: Fragment, name: String) {
    supportFragmentManager.beginTransaction()
            .replace(R.id.container, fragment, name)
            .addToBackStack(name).commit()
}

Note: Using replace fragment is preferred over using add, as it doesn’t store unnecessary fragments in the memory, and also reduce the UI rendering need. However if the current fragment contains a webview, to open the next fragment, using add is better, this is in the event when you pop that “next” fragment, your existing fragment with webview is retain and doesn’t need to be reloaded.

Example App

That’s my app behavior. To illustrate this better, I create an app with 8 buttons, 4 buttons is adding Fragment 1–4, and 4 buttons is for replacing with Fragment 1–1.

The code could be accessed below.

The issue illustration

With the App above, as you click on any button, a fragment will be added or replaced onto the activity container. For either of it (add or replace), they are track in the back-stack so that user could pop them back in a reverse order.

To illustrate this better, you could click

Add 4 fragments

Add Fragment 1 → Add Fragment 2 → Add Fragment 3 → Add Fragment 4, then you’ll have 4 fragments stack up in the container.

All 4 fragments in Container

When you press back button on the menu (i.e. the ← key on the top left), each of the fragment will get pop up, till it reach the original state (where there is no fragment).

This is all good.

The other scenario would be

Replace 4 fragments

Replace with Fragment 1 → Replace with Fragment 2 → Replace with Fragment 3 → Replace with Fragment 4, then you’ll have one fragment i.e. fragment 4 remaining.

Fragment 4 in container, while fragment 1–3 at backstack

As you press the back button on the menu (i.e. the ← key on the top left), the fragment in the container will get pop, replace by the previous fragment, one at a time. All this is good, as we could reach the original state where there’s no fragment in the container.

This is all good as well.

Some puzzle time — Mixing Add and Replace in adding to the stack

There are various possible combination when mixing the add and replace. In some of these combination, when we start popping the fragment, it would got stuck at some point (i.e. press the ← key on the top left, doesn’t pop the fragment 💥).

The workaround is, use the Hardware key back, still can pop the fragment. It’s just that the toolbar ← no longer works. Strange strange behavior.

Let me pick a few combinations and tell you which is fine and which is not (I’ll abbreviate add fragment x as Add x and replace with fragment x as Rep x, and press ← to pop as pop).

Based on the result, see if you could detect the pattern of the combination that cause the issue?

  1. Rep 1Rep 2Add 3Add 4 : Pop all 4 fragments fine.
  2. Add 1Rep 2Add 3Add 4 : Pop all 4 fragments fine.
  3. Add 1Rep 2Rep 3Rep 4 : Pop all 4 fragments fine.
  4. Add 1Rep 2Rep 3Add 4 : Pop all 4 fragments fine.
  5. Add 1Add 2Rep 3Add 4 : Pop stuck at fragment 1 💥
  6. Rep 1Add 2Rep 3Add 4 : Pop stuck at fragment 1 💥
  7. Rep 1Add 2Add 3Rep 4 : Pop stuck at fragment 2 💥
  8. Add 1Rep 2Add 3Rep 4 : Pop stuck at fragment 2 💥

Can you discover the pattern causing causing the issue from above? Treat it like a little puzzle solving problem.

If you think the given combinations above are not sufficient, to generate more patterns for the puzzle, you could compile the given code above, and test out. See if you could identified the problematic issue. Happy hunting …

Tips, when your pop using ← button on toolbar fails, you could use the hardware back button, it should still able to pop it.

Don’t worry if you can’t get it, the answer is below 😉.

Demo illustration

Anyway, for those just like to read, to illustrate the issue more clearly. Below is the video showing 3 scenarios

  1. Add 1Add 2Add 3Add 4 : All pop well. (note the number overlap as the background is transparent to show each fragment added on top of each other)
  2. Rep 1Rep 2Rep 3Rep 4 : All pop well (the number no longer overlap, as the fragment was replaced)
  3. Add 1Rep 2Add 3Rep 4 : The pop stuck at Fragment 2 💥. Why?!!?

The troubling pattern identified — answer to the puzzle.

After some investigation, found the troubling pattern causing the issue.

The pattern is for every add and replace that has a fragment already shown before that, the pop will stuck at that fragment. e.g.

Add or Rep 1Add 2Rep 3. Then the pop will stuck at 1.

You could reverify the above 8 combinations given, and affirm that this pattern is exhibited in the 4 combinations that cause the issue, and not on the other 4.

Deep dive into the issue

With the troubling pattern identified, now it would be easier to consistently duplicate the issue using the minimal steps that reproduce the issue. Let’s use the flow below.

Rep 1Add 2Rep 3

To deep dive, let’s trace how the fragment stack works.

Step 1 : Rep 1 : Replace the empty container with Fragment 1. The diagram below illustrated.

Step 2: Add 2 : Add Fragment 2, on top of Fragment 1. The diagram below illustrated.

Step 3: Rep 3 : Replace the container with Fragment 3. With that, the Fragment 1 and 2 was push out of the container.

Step 4: Pop 3 : Press the ← key on the top left, to pop Fragment 3 out. When Fragment 3 is pop out, Fragment 1 and 2 is brought back into the Container by the System.

Step 5: Pop 2 : Press the ← key on the top left again, to pop Fragment 2 out. Fragment 1 is still retained in Container.

Step 6: Pop 1 💥 : Press the ← key on the top left again, expect to pop Fragment 1, but nothing happens??!! 💥. The ← key on the top left no longer works!! Why why?

The culprit of the problem

Apparently the culprit of the issue is not in step 6. The trigger point happens in step 4 above. That’s when both Fragment 1 and 2 is got restored into the Container.

At that time both fragment got recreated. When both fragment got recreated, both fragment call to

(activity as AppCompatActivity).setSupportActionBar(toolbar)

Ideally this should work, as the sequence of the call is still correct where Fragment 1 got restored first, and followed by Fragment 2. However for some reason , given that both called at almost a very similar time, the toolbar for fragment 1 doesn’t work anymore.

I highly suspect this is the issue with the SDK library on setSupportActionBar. I filed an issue to Google in https://issuetracker.google.com/issues/73793626

The workaround of the issue

Given this seems like a google bug to me, I don’t think I have a good resolution to it. I can’t also wait for Google, as so far based on experience, it takes month for any reported issue to fix unless it’s super obvious that affects everyone.

So I have to workaround.

From using the App point of view, using the Hardware Back Button still able to trigger the onBackPressed() function in the Activities, and hence popping the fragment is still possible. But user really expect to press the ← key on the top left to pop the stack.

So how?

Code that makes the toolbar came alive

I also discover that if there’s a change upon Fragment 2 got pop, if I could let Fragment 1 call the below again, the toolbar will come back to life.

(activity as AppCompatActivity).setSupportActionBar(toolbar)

However when Fragment 2 got pop, no codes in Fragment 1 will get triggered, given that Fragment 1 is already alive within Container.

The workable workaround

With the above idea, the approach I use is, whenever a pop happens, let the fragment at the top level call this

(activity as AppCompatActivity).setSupportActionBar(toolbar)

This would ensure the top level fragment always has it’s toolbar reset and come alive.

To detect the fragment pop I use the below in my activity, calling the onResume of the top fragment.

supportFragmentManager.addOnBackStackChangedListener {
    val currentFragment = supportFragmentManager
                            .findFragmentById(R.id.container)
    currentFragment?.onResume()
}

Note the onBackStackChangedListener get trigger on both push and pop of the fragment. I didn’t differentiate them there.

For all the fragment I explicitly make the onResume as below

override fun onResume() {
    super.onResume()
    (activity as AppCompatActivity)
                            .setSupportActionBar(toolbar_actionbar)
}

With these code in, all my push and pop of the fragment, regardless of using add or replace, in any sequence, can be pop with the ← button on the toolbar now.

I don’t think my workaround is ideal as well. But at least it get the things working. If you ever know there’s a better resolution to this issue, please feel free to share. You could also post it on stackoverflow as below

If not, sincerely hopes Google get some resolution to fix this issue though what I reported in https://issuetracker.google.com/issues/73793626

Hope this post is helpful and you appreciate it. Do share with others.

You could check out my other interesting topics here.

Follow me on medium, Twitter or Facebook for issues discovered, little tips and learning on Android, Kotlin etc related topics. ~Elye~

Android
Android App Development
AndroidDev
Google
Mobile App Development
Recommended from ReadMedium