Search code examples
androidandroid-fragmentsandroid-preferences

PreferenceFragmentCompat with custom layout not finding toolbar on API 28+


I have a single activity app which uses a settings fragment (PreferenceFragmentCompat) with a custom layout (set via overriding the PreferenceThemeOverlay style). From the fragment's onViewCreated method I'm using setSupportActionBar to reference the toolbar which is present on that layout. This works fine until it runs on API 28+, at which point the reference to toolbarSettings is null and an error is raised.

java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.appcompat.app.ActionBar.setTitle(java.lang.CharSequence)' on a null object reference
        at androidx.navigation.ui.ActionBarOnDestinationChangedListener.setTitle(ActionBarOnDestinationChangedListener.java:48)
        at androidx.navigation.ui.AbstractAppBarOnDestinationChangedListener.onDestinationChanged(AbstractAppBarOnDestinationChangedListener.java:104)
        at androidx.navigation.NavController.addOnDestinationChangedListener(NavController.java:204)
        at androidx.navigation.ui.NavigationUI.setupActionBarWithNavController(NavigationUI.java:228)
        at androidx.navigation.ui.ActivityKt.setupActionBarWithNavController(Activity.kt:74)
        at androidx.navigation.ui.ActivityKt.setupActionBarWithNavController$default(Activity.kt:89)
        at com.simplenotes.notes.presentation.ui.SettingsFragment.setupActionBar(SettingsFragment.kt:39)
        at com.simplenotes.notes.presentation.ui.SettingsFragment.onViewCreated(SettingsFragment.kt:20)
        at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:332)

I've seen mention of changes to custom preference fragments in this version however can't see anything specific to this behaviour. Does anyone know a solution or can point out what has been done wrong?

SettingsFragment.kt

class SettingsFragment : PreferenceFragmentCompat() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupActionBar()
    }

    private fun setupActionBar() {
        setHasOptionsMenu(true)

        val hostActivity = requireActivity() as AppCompatActivity
        hostActivity.setSupportActionBar(toolbarSettings)
        hostActivity.setupActionBarWithNavController(findNavController())

        val actionBar = hostActivity.supportActionBar
        actionBar?.title = resources.getString(R.string.title_settings)
        actionBar?.setDisplayHomeAsUpEnabled(true)
    }
}

fragment_settings.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbarSettings"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbarSettings"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize" />

    </com.google.android.material.appbar.AppBarLayout>

    <FrameLayout
        android:id="@android:id/list_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

styles.xml

<resources>
    <style name="Theme.MaterialComponents.DayNight.NoActionBar.Bridge" parent="Theme.MaterialComponents.Light.NoActionBar.Bridge" />

    <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar.Bridge">
        <item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item>
    </style>

    <style name="AppTheme.PreferenceThemeOverlay" parent="PreferenceThemeOverlay">
        <item name="android:layout">@layout/fragment_settings</item>
    </style>
</resources>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.simplenotes.notes">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".presentation.ui.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Dependencies

implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.1.0'

Solution

  • In case anyone else runs into this issue I found the problem.

    In addition to the styles.xml file in the question, there was also a styles-v27.xml file which was being used to override the windowLightNavigationBar attribute...

    <resources>
        <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar.Bridge">
            <item name="android:windowLightNavigationBar">false</item>
        </style>
    </resources>
    

    On API v28 though this meant the preferenceTheme value wasn't getting picked up. When the settings fragment was inflated it effectively didn't have the custom layout, hence the toolbar not being created.

    In styles-v27.xml I simply repeated the preferenceTheme attribute like below and hey presto it works fine on v28 and above.

    <resources>
        <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar.Bridge">
            <item name="android:windowLightNavigationBar">false</item>
    
            <item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item>
        </style>
    </resources>