Nick's blog Nothing to see here?

Caution: PreferenceFragmentCompat enables vertical scroll bars in your activity theme

2024-04-02
Nick

TL;DR: PreferenceFragmentCompat.onCreate() mutates your activity theme to set android:scrollbars to vertical. This can cause unexpected behavior and major performance issues in specific scenarios.

I was recently investigating a bug in the Gramophone app I contribute to which got described to me as “RecyclerView scrolling is extremely laggy after changing to night mode”. I tried reproducing it, but toggling night mode on with the quick settings tile resulted in no change at all. I was only able to reproduce the issue after using the AppCompatDelegate.setDefaultNightMode()-based toggle in the app’s settings. Most curiously, however, the issue disappeared once I restarted the app, even though AppCompatDelegate.setDefaultNightMode() was still present in Application.onCreate() and successfully set the night mode. At first, I had suspected the live theme reload behavior, but I went on to debug why the performance was decreasing first.

Profiler graph showing more extreme bars after the night mode was changed

As you can see in this profiler graph, after I changed to night mode (around middle of the graph), scrolling takes more CPU than it did before. At that point, I identified View.awakenScrollbars() as the culprit and was even more confused. In the bottom-top view, I was able to see this being called during layout of RecyclerView and during RecyclerView.addViewInt(). The second one hinted at the child Views of the RecyclerView taking a lot of time in awakenScrollbars, this however only is possible if the child View has either horizontal or vertical scroll bars enabled (a large part of the time spent in View.awakenScrollbars() was spent in Handler.removeCallbacks() which is only called if scroll bars are enabled).

I then experimented with calling Activity.recreate() instead of AppCompatDelegate.setDefaultNightMode() in an attempt to isolate the issue and I found that I can still reproduce the issue. Because, for example, rotation - which internally does something similar to Activity.recreate() - does not reproduce the issue, I tried to isolate the sole difference (which is that I rotate on main screen, but recreate() in PreferenceFragmentCompat). This made me figure out that calling getParentFragmentManager().popBackStackImmediate() until the current fragment is my main fragment removed the scroll bars (and hence fixed the lag). I then, in an attempt to understand how that happens, searched "vertical" in the androidx repositories and found that it is set in BasePreferenceTheme. It then clicked and I figured out that the ?preferenceTheme is applied to the global activity theme by PreferenceFragmentCompat (which the code indeed does). The lag can also be observed by just entering a preference fragment, quitting again and then changing the view type to grid (which re-inflates all RecyclerView.ViewHolders).

To workaround this, I added this code to my subclass of PreferenceFragmentCompat:

    override fun onDestroy() {
        // Work around b/331383944: PreferenceFragmentCompat permanently mutates activity theme (enables vertical scrollbars)
        requireContext().theme.applyStyle(R.style.Theme_Gramophone, true)
        super.onDestroy()
    }

This workaround has a possible pitfall: Your own theme NEEDS to set

    <!-- Work around b/331383944: PreferenceFragmentCompat permanently mutates activity theme (enables vertical scrollbars) -->
    <item name="android:scrollbars">none</item>

because Context.getTheme().applyStyle() retains previous theme values if the current one does not override it (or if you don’t set the force parameter to true!).


Previous: BatterySaverToggle

Similar Posts

Comments