androidkotlinpreferencefragment

PreferenceFragmentCompat works while testing but crashes on published app


I have run into a very strange error with PreferenceFragmentCompat. I am splitting my preferences into multiple screens using the guide here. This works as expected while testing using AVD and testing using my device connected to Android Studio. However, when I publish the app to the Play Store and run the published version, it crashes each time I try to click on one of the multiple settings. I've confirmed the crash on both Android 8 and 10. I have no idea why it works as expected while testing, but crashes in the published version? I am publishing to a closed alpha track. I've tried using the "send feedback" option on crash to view the crash report, but it is not available in the Play Console so I can't track down what is going on. Below is the code:

build.gradle (Module: app)
...
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.preference:preference:1.1.1'
...
MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var settingsButton: ImageButton

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        settingsButton = findViewById(R.id.settingsButtonMain)
        settingsButton.setOnClickListener {
            settingsClicked()
        }
        ...
    }

    private fun settingsClicked() {
        val settingsIntent = Intent(this, SettingsActivity::class.java)
        startActivity(settingsIntent)
    }
}
SettingsActivity.kt

class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        supportFragmentManager.beginTransaction().replace(R.id.settings, SettingsFragment(this)).commit()
        supportFragmentManager.addOnBackStackChangedListener {
            if(supportFragmentManager.backStackEntryCount == 0) {
                title = "App Settings"
            }
        }
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        ...
    }

    class SettingsFragment(cont: Context) : PreferenceFragmentCompat() {
        private var context1: Context = cont

        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.root_preferences, rootKey)
        }
    }

    class Screen1PreferencesFragment : PreferenceFragmentCompat() {
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.screen1_preferences, null)
        }
    }

    class Screen2PreferencesFragment : PreferenceFragmentCompat() {
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.screen2_preferences, null)
        }
    }

    override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference): Boolean {
        val args: Bundle = pref.extras
        val fragment: Fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, pref.fragment)
        fragment.arguments = args
        fragment.setTargetFragment(caller, 0)
        supportFragmentManager.beginTransaction().replace(R.id.settings, fragment).addToBackStack(null).commit()

        return true
    }

    override fun onSupportNavigateUp(): Boolean {
        if(supportFragmentManager.popBackStackImmediate()) {
            return true
        }

        return super.onSupportNavigateUp()
    }
}
root_preferences.xml

<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory app:title="App Settings">
        <Preference
            app:fragment="com.example.TestApp.SettingsActivity$Screen1PreferencesFragment"
            app:summary="Multi pref screen1"
            app:title="Screen 1" />

        <Preference
            app:fragment="com.example.TestApp.SettingsActivity$Screen2PreferencesFragment"
            app:summary="Multi pref screen2"
            app:title="Screen 2" />
    </PreferenceCategory>
</PreferenceScreen>
screen1_preferences.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <PreferenceCategory app:title="Screen 1">
        <Preference
            android:clickable="false"
            app:enabled="false"
            app:summary="This is screen 1"
            app:title="Screen 1" />
    </PreferenceCategory>
</PreferenceScreen>
screen2_preferences.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <PreferenceCategory app:title="Screen 2">
        <Preference
            android:clickable="false"
            app:enabled="false"
            app:summary="This is screen 2"
            app:title="Screen 2" />
    </PreferenceCategory>
</PreferenceScreen>

On app publish, Google's auto testers reported the below crash stack trace which may be related:

androidx.fragment.app.Fragment$InstantiationException: 
  at androidx.fragment.app.FragmentFactory.loadFragmentClass (FragmentFactory.java:57)
  at androidx.fragment.app.Fragment.instantiate (Fragment.java:8)
  at androidx.fragment.app.FragmentContainer.instantiate (FragmentContainer.java:2)
  at androidx.fragment.app.FragmentManager$3.instantiate (FragmentManager.java:2)
  at com.example.TestApp.SettingsActivity.onPreferenceStartFragment (SettingsActivity.java:2)
  at androidx.preference.PreferenceFragmentCompat.onPreferenceTreeClick (PreferenceFragmentCompat.java:8)
  at androidx.preference.Preference.performClick (Preference.java:8)
  at androidx.preference.Preference.performClick (Preference.java:8)
  at androidx.preference.Preference$1.onClick (Preference.java:2)
  at android.view.View.performClick (View.java:7140)
  at android.view.View.performClickInternal (View.java:7117)
  at android.view.View.access$3500 (View.java:801)
  at android.view.View$PerformClick.run (View.java:27351)
  at android.os.Handler.handleCallback (Handler.java:883)
  at android.os.Handler.dispatchMessage (Handler.java:100)
  at android.os.Looper.loop (Looper.java:214)
  at android.app.ActivityThread.main (ActivityThread.java:7356)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:930)
Caused by: java.lang.ClassNotFoundException: 
  at java.lang.Class.classForName (Class.java)
  at java.lang.Class.forName (Class.java:454)
  at androidx.fragment.app.FragmentFactory.loadClass (FragmentFactory.java:2)
  at androidx.fragment.app.FragmentFactory.loadFragmentClass (FragmentFactory.java:2)
  at androidx.fragment.app.Fragment.instantiate (Fragment.java:8)
  at androidx.fragment.app.FragmentContainer.instantiate (FragmentContainer.java:2)
  at androidx.fragment.app.FragmentManager$3.instantiate (FragmentManager.java:2)
  at com.example.TestApp.SettingsActivity.onPreferenceStartFragment (SettingsActivity.java:2)
  at androidx.preference.PreferenceFragmentCompat.onPreferenceTreeClick (PreferenceFragmentCompat.java:8)
  at androidx.preference.Preference.performClick (Preference.java:8)
  at androidx.preference.Preference.performClick (Preference.java:8)
  at androidx.preference.Preference$1.onClick (Preference.java:2)
  at android.view.View.performClick (View.java:7140)
  at android.view.View.performClickInternal (View.java:7117)
  at android.view.View.access$3500 (View.java:801)
  at android.view.View$PerformClick.run (View.java:27351)
  at android.os.Handler.handleCallback (Handler.java:883)
  at android.os.Handler.dispatchMessage (Handler.java:100)
  at android.os.Looper.loop (Looper.java:214)
  at android.app.ActivityThread.main (ActivityThread.java:7356)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:930)

Edit: For future reference - I was able to get the full error log:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.TestApp, PID: 20293
    androidx.fragment.app.Fragment$e: Unable to instantiate fragment com.example.TestApp.SettingsActivity$Screen1PreferencesFragment: make sure class name exists
        at a.j.d.i.d(:91)
        at androidx.fragment.app.Fragment.instantiate(:546)
        at a.j.d.f.a(:57)
        at a.j.d.m$c.a(:390)
        at com.example.TestApp.SettingsActivity.e(:370)
        at a.n.g.q(:407)
        at androidx.preference.Preference.h0(:1192)
        at androidx.preference.Preference.i0(:1166)
        at androidx.preference.Preference$a.onClick(:181)
        at android.view.View.performClick(View.java:7125)
        at android.view.View.performClickInternal(View.java:7102)
        at android.view.View.access$3500(View.java:801)
        at android.view.View$PerformClick.run(View.java:27336)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: java.lang.ClassNotFoundException: com.example.TestApp.SettingsActivity$Screen1PreferencesFragment
        at java.lang.Class.classForName(Native Method)
        at java.lang.Class.forName(Class.java:454)
        at a.j.d.i.c(:47)
        at a.j.d.i.d(:88)
        at androidx.fragment.app.Fragment.instantiate(:546) 
        at a.j.d.f.a(:57) 
        at a.j.d.m$c.a(:390) 
        at com.example.TestApp.SettingsActivity.e(:370) 
        at a.n.g.q(:407) 
        at androidx.preference.Preference.h0(:1192) 
        at androidx.preference.Preference.i0(:1166) 
        at androidx.preference.Preference$a.onClick(:181) 
        at android.view.View.performClick(View.java:7125) 
        at android.view.View.performClickInternal(View.java:7102) 
        at android.view.View.access$3500(View.java:801) 
        at android.view.View$PerformClick.run(View.java:27336) 
        at android.os.Handler.handleCallback(Handler.java:883) 
        at android.os.Handler.dispatchMessage(Handler.java:100) 
        at android.os.Looper.loop(Looper.java:214) 
        at android.app.ActivityThread.main(ActivityThread.java:7356) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 
     Caused by: java.lang.ClassNotFoundException: Didn't find class "com.example.TestApp.SettingsActivity$Screen1PreferencesFragment" on path: DexPathList[[zip file "/data/app/com.example.TestApp-Y6977dm5oijKMZmKaT4_3A==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.TestApp-Y6977dm5oijKMZmKaT4_3A==/lib/x86, /system/lib, /system/product/lib]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:196)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at java.lang.Class.classForName(Native Method) 
        at java.lang.Class.forName(Class.java:454) 
        at a.j.d.i.c(:47) 
        at a.j.d.i.d(:88) 
        at androidx.fragment.app.Fragment.instantiate(:546) 
        at a.j.d.f.a(:57) 
        at a.j.d.m$c.a(:390) 
        at com.example.TestApp.SettingsActivity.e(:370) 
        at a.n.g.q(:407) 
        at androidx.preference.Preference.h0(:1192) 
        at androidx.preference.Preference.i0(:1166) 
        at androidx.preference.Preference$a.onClick(:181) 
        at android.view.View.performClick(View.java:7125) 
        at android.view.View.performClickInternal(View.java:7102) 
        at android.view.View.access$3500(View.java:801) 
        at android.view.View$PerformClick.run(View.java:27336) 
        at android.os.Handler.handleCallback(Handler.java:883) 
        at android.os.Handler.dispatchMessage(Handler.java:100) 
        at android.os.Looper.loop(Looper.java:214) 
        at android.app.ActivityThread.main(ActivityThread.java:7356) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 
I/Process: Sending signal. PID: 20293 SIG: 9
Disconnected from the target VM, address: 'localhost:8607', transport: 'socket'

Solution

  • Thanks to @heX for pointing me in the right direction on this one. To replicate the error in Android Studio AVD, add the following code in buildTypes in build.gradle (Module: app)

    buildTypes {
        debug {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    

    Run your app in debug mode and it will display the error when you trip it. The solution in this particular case is to add the following to the proguard-rules.pro file:

    -keep class * extends androidx.fragment.app.Fragment { public *; }
    

    I'll leave it to someone intelligent who can explain why that works. Alternatively you could set minifyEnabled false in the build.gradle if you don't mind losing the benefits of minification.