How to open a new PreferenceFragment from current one, using the new Android-X API?
Asked Answered
G

3

9

Background

On previous versions of support library, we could use headers in order to have a main-menu screen of settings, that each would open a new settings screen (fragment) .

The problem

Now headers are gone (as written here) for some time, and I think it became worse on android-x :

One thing you’ll note isn’t in here is preference headers and you’d be totally right. However, that doesn’t mean a single list of preferences need to span a 10” tablet screen. Instead, your Activity can implement OnPreferenceStartFragmentCallback (link) to handle preferences with an app:fragment attribute or OnPreferenceStartScreenCallback (link) to handle PreferenceScreen preferences. This allows you to construct a ‘header’ style PreferenceFragmentCompat in one pane and use those callbacks to replace a second pane without working in two separate types of XML files.

Thing is, I fail to use these on the new android-x API.

Each fragment has its own preferences XML tree (using setPreferencesFromResource within onCreatePreferences) , but each solution I've come up with has either done nothing, or crashed.

To put it in a visual way, this is what I'm trying to achieve :

enter image description here

Since there are multiple sub settings screens, it would be very messy to have all of the preferences of all of them be put in one XML file of the main settings screen.

What I've tried

Only thing I've succeeded, is to use the PreferenceScreen to hold the preferences of the sub-screen that's supposed to be shown.

Here's a working code (project available here) of such a thing :

preferences.xml

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="Demo">

    <PreferenceScreen
        android:key="screen_preference" android:summary="Shows another screen of preferences"
        android:title="Screen preferenc">

        <CheckBoxPreference
            android:key="next_screen_checkbox_preference"
            android:summary="Preference that is on the next screen but same hierarchy"
            android:title="Toggle preference"/>

    </PreferenceScreen>

</PreferenceScreen>

MainActivity.kt

class MainActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        if (savedInstanceState == null)
            supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
    }

    override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen): Boolean {
        val f = PrefsFragment()
        val args = Bundle(1)
        args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.key)
        f.arguments = args
        supportFragmentManager.beginTransaction().replace(android.R.id.content, f).addToBackStack(null).commit()
        return true
    }

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

But, as I wrote, this is not what I'm trying to do. I want to have multiple classes that extend PreferenceFragmentCompat, each with its own XML file, which will be opened from the main one.

Here are the things I've tried (and failed) :

  1. Set a "android:fragment" for the PreferenceScreen, to point to the new fragments classes, similar to headers. This didn't do anything at all.

  2. Use a normal Preference and have click listener for it, that will do the fragment transaction as shown on the original code. This caused a crash that says something like "Preference object with key screen_preference is not a PreferenceScreen" .

  3. Tried to avoid using ARG_PREFERENCE_ROOT , but had same crash as on #2 .

  4. As suggested here, I tried to return this in function getCallbackFragment, but this didn't help at all.

The question

Is it possible to have the main settings fragment just let the user to navigate to the other fragments, while not having any other preferences that belong to them (inside preferences.xml) ?

How?

Grith answered 9/10, 2018 at 14:39 Comment(0)
N
3

What you tried in 1) was the correct approach - but you should not use <PreferenceScreen> tags for this.

Your XML resource should look like this instead:

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    <Preference
        app:key="screen_preference" 
        app:summary="Shows another screen of preferences"
        app:title="Screen preference"
        app:fragment="com.example.user.myapplication.MainActivity$PrefsFragment2"/>

</PreferenceScreen>

Also, if you are using a version of Preference older than androidx.preference:preference:1.1.0-alpha01, you will need to implement onPreferenceStartFragment to handle the fragment transaction. (in 1.1.0 alpha01 this method has a default implementation, but you are still encouraged to use your own implementation to customize any animations / transitions)

This should look something like:

override fun onPreferenceStartFragment(
        caller: PreferenceFragmentCompat,
        pref: Preference
): Boolean {
    // Instantiate the new Fragment
    val args = pref.extras
    val fragment = supportFragmentManager.fragmentFactory.instantiate(
            classLoader,
            pref.fragment,
            args
    ).apply {
        arguments = args
        setTargetFragment(caller, 0)
    }
    // Replace the existing Fragment with the new Fragment
    supportFragmentManager.beginTransaction()
            .replace(R.id.settings, fragment)
            .addToBackStack(null)
            .commit()
    return true
}

For more information you can check out the Settings guide and the AndroidX Preference Sample


EDIT: a sample of the first solution, after updating, available here.

Here's how it can work (sample available here) :

MainActivity.kt

class MainActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
    override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
        //Note: this whole function won't be needed when using new version of fragment dependency (1.1.0 and above)
        val fragment = Fragment.instantiate(this, pref.fragment, pref.extras)
        fragment.setTargetFragment(caller, 0)
        supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack(null).commit()
        return true
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        if (savedInstanceState == null)
            supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
    }

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

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

preferences.xml

  <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">

    <Preference
      app:fragment="com.example.user.myapplication.MainActivity$PrefsFragment2" app:key="screen_preference" app:summary="Shows another screen of preferences"
      app:title="Screen preference"/>

  </PreferenceScreen>

preferences2.xml

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="Demo">

  <PreferenceCategory android:title="Category">
    <CheckBoxPreference
      android:key="next_screen_checkbox_preference" android:summary="AAAA" android:title="Toggle preference"/>
  </PreferenceCategory>

</PreferenceScreen>

gradle dependencies:

implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.0.0'
Neile answered 11/12, 2018 at 14:0 Comment(9)
I don't see onPreferenceStartFragment available anywhere. Can you please show how you got to use it? Also, the solution of 1.1.0-alpha01 seems to work well (without the need for onPreferenceStartFragment). But be careful when using this alpha version. It seems to cause crashes : issuetracker.google.com/issues/120687886Grith
Anyway, you got the answer right, so I've marked it as the correct one. Thank you.Grith
The method is part of this interface: developer.android.com/reference/androidx/preference/…Neile
Can you please show the full code, then ? Use mine, and just modify itGrith
Your code is fine, you are just implementing the wrong interface: PreferenceFragmentCompat.OnPreferenceStartScreenCallback and override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen) Instead just rename this to: PreferenceFragmentCompat.OnPreferenceStartFragmentCallback and override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference) And it will work fineNeile
I don't seefragmentFactory. How did you call it? And my code of onPreferenceStartScreen had a call to create PrefsFragment2 directly instead of using what's on the Preference itself which tells which fragment to go to.Grith
Maybe as such: s000.tinyupload.com/?file_id=35271103952026251688 ?Grith
fragmentFactory is new in Fragment 1.1.0*, see the release notes. If you are not using Fragment 1.1.0*, then you should use (this is now deprecated in 1.1.0) this instead - similar to your changed code: final Fragment fragment = Fragment.instantiate(getContext(), preference.getFragment(), preference.getExtras());Neile
version Fragment 1.1.0 is quite problematic at the moment. I've tried what you wrote, and it seems to work very well. Gave you +1 for the effort. I decided to edit your answer to have the full solutionGrith
L
6

FYI, if you are using Navigation drawer + androidx.appcompat, you can:

1) Split each PreferenceScreen child into as many as preference.xml file: ie "Pref_general.xml" will be the main preference and "pref_ServerSettings.xml" contains the PreferenceScreen child with you server settings. 2) Create a PreferenceFragmentCompat for each preference.xml:

"PrefFragmentGeneral"

On your PrefFragmentGeneral.xml file, add a Preference instead of a PreferenceScreen like bellow, for any sub xml:

<Preference
    android:key="pref_serverPref"
    android:summary="@string/settings_serverPrefSum"
    android:title="@string/settings_serverPrefTitle"
    />

"PrefFragmentServer"

2) Ensure you override "onCreatePreferences" to set preferences from the XML file you would like:

public class PrefFragmentGeneral extends PreferenceFragmentCompat {
    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        setPreferencesFromResource(R.xml.Pref_general, rootKey);
        //find your preference(s) using the same key
        Preference serverPref=findPreference("pref_serverPref");
        if(serverPref!=null){
            //Assign the click listener to navigate to the fragment using the navigation controller
            serverPref.setOnPreferenceClickListener(preference -> {
                NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment);
                navController.navigate(R.id.nav_PrefFragmentServer);
                return true;
            });
        }
    }
//and the PrefFragmentServer 
public class PrefFragmentServer extends PreferenceFragmentCompat {
    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        setPreferencesFromResource(R.xml.pref_ServerSettings,rootKey);
     }
}

3) Register all your fragments inside your navigation drawer:

Navigation drawer

Now enjoy!

Pros: When you navigate back, you go back to the "General" preference as if you where coming back to a PreferenceActivity children! AND you don't get an exception telling you that the fragment is not part of FragmentManager.

Lingerie answered 14/1, 2020 at 11:4 Comment(5)
Nice . I wonder though about something: Is there an official way to handle it for large screens? I remember in the past that Google recommended to have 2 fragments at the same time. How does it work today (a long time since I used a tablet) ?Grith
I know that you can insert sub fragments inside a fragment, so i suppose you should create a fragment for wider screens and include your two "phone" fragments inside...Lingerie
I know. I just hoped for an official way to do it. I remember there was a wizard for it a very long time ago.Grith
I was frustrated for 2 days. Saves me a hell lot. Thanks.Seamy
Thank you! I would have been happier by using the official 'app:fragment' but you probably saved me a lot of time.Gnaw
G
3

OK, I've found 2 possible, yet weird, solutions.

I still would like to know if there is an official way to do it, because both solutions are quite weird.

Solution 1

In the main settings preference XML file, for each sub PreferenceScreen, I put an empty Preference tag.

preferences.xml

<PreferenceScreen
    android:key="screen_preference" android:summary="Shows another screen of preferences"
    android:title="Screen preference">
    <Preference/>
</PreferenceScreen>

I pass null for the second argument of setPreferencesFromResource on the new sub-screen fragment.

Here's the code (project available here) :

MainActivity.kt

class MainActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        if (savedInstanceState == null)
            supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
    }

    override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen): Boolean {
        supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment2()).addToBackStack(null).commit()
        return true
    }

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

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

Of course, this needs to be modified so that you will know which fragment to create and add...

Solution 2

I use a normal Preference instead of each PreferenceScreen, and for each of them I choose to add the fragment upon clicking (project available here) :

preferences.xml

<Preference
    android:key="screen_preference" android:summary="Shows another screen of preferences"
    android:title="Screen preference"/>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        if (savedInstanceState == null)
            supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
    }

    class PrefsFragment : PreferenceFragmentCompat() {
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.preferences, rootKey)
            setPreferenceToOpenFragmentAsNewPage(findPreference("screen_preference"), PrefsFragment2::class.java)
        }

        private fun setPreferenceToOpenFragmentAsNewPage(pref: Preference, java: Class<out PreferenceFragmentCompat>) {
            pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                val fragment = java.newInstance()
                val args = Bundle(1)
                args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.key)
                fragment.arguments = args
                activity!!.supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack(null).commit()
                true
            }
        }
    }

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

EDIT: a tiny modification to the second solution can make it nicer:

preferences.xml

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="Demo">

    <Preference
        android:fragment="com.example.user.myapplication.MainActivity$PrefsFragment2" android:key="screen_preference"
        android:summary="Shows another screen of preferences" android:title="Screen preference"/>

</PreferenceScreen>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        if (savedInstanceState == null)
            supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
    }

    class PrefsFragment : PreferenceFragmentCompat() {
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.preferences, rootKey)
            setPreferenceToOpenFragmentAsNewPage(findPreference("screen_preference"))
        }

        private fun setPreferenceToOpenFragmentAsNewPage(pref: Preference) {
            pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                val clazz = Class.forName(pref.fragment)
                val fragment: PreferenceFragmentCompat = clazz.newInstance() as PreferenceFragmentCompat
                val args = Bundle(1)
                args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.key)
                fragment.arguments = args
                activity!!.supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack(null).commit()
                true
            }
        }
    }

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

}

Note that you need to add this to Proguard rules:

-keepnames public class * extends androidx.preference.PreferenceFragmentCompat

Another improvement to solution #2 is that it can go over all preferences by itself:

class PrefsFragment : BasePreferenceFragment() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.preferences_headers, rootKey)
        val preferenceScreen = preferenceScreen
        val preferenceCount = preferenceScreen.preferenceCount
        for (i in 0 until preferenceCount) {
            val pref = preferenceScreen.getPreference(i)
            val fragmentClassName = pref.fragment
            if (fragmentClassName.isNullOrEmpty())
                continue
            pref.setOnPreferenceClickListener {
                showPreferenceFragment(activity!!, fragmentClassName)
                true
            }
        }
    }
}

companion object {
    @JvmStatic
    private fun showPreferenceFragment(activity: FragmentActivity, fragmentClassName: String) {
        val clazz = Class.forName(fragmentClassName)
        val fragment: PreferenceFragmentCompat = clazz.newInstance() as PreferenceFragmentCompat
        val fragmentsCount = activity.supportFragmentManager.fragments.size
        val transaction = activity.supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment)
        if (fragmentsCount > 0)
            transaction.addToBackStack(null)
        transaction.commit()
    }
}

EDIT: seems the first solution was the correct one, but needed a change. Check the answer here. Full sample available here.

Grith answered 10/10, 2018 at 8:43 Comment(2)
Out of interest, did you find anything useful (regarding this) in the Android Dev Summit? youtu.be/PS9jhuHECEQ?t=546Divisive
@Divisive I didn't, but I tried now, and it still doesn't work. Any idea why? Here's a sample project: s000.tinyupload.com/index.php?file_id=35195751785067477402Grith
N
3

What you tried in 1) was the correct approach - but you should not use <PreferenceScreen> tags for this.

Your XML resource should look like this instead:

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    <Preference
        app:key="screen_preference" 
        app:summary="Shows another screen of preferences"
        app:title="Screen preference"
        app:fragment="com.example.user.myapplication.MainActivity$PrefsFragment2"/>

</PreferenceScreen>

Also, if you are using a version of Preference older than androidx.preference:preference:1.1.0-alpha01, you will need to implement onPreferenceStartFragment to handle the fragment transaction. (in 1.1.0 alpha01 this method has a default implementation, but you are still encouraged to use your own implementation to customize any animations / transitions)

This should look something like:

override fun onPreferenceStartFragment(
        caller: PreferenceFragmentCompat,
        pref: Preference
): Boolean {
    // Instantiate the new Fragment
    val args = pref.extras
    val fragment = supportFragmentManager.fragmentFactory.instantiate(
            classLoader,
            pref.fragment,
            args
    ).apply {
        arguments = args
        setTargetFragment(caller, 0)
    }
    // Replace the existing Fragment with the new Fragment
    supportFragmentManager.beginTransaction()
            .replace(R.id.settings, fragment)
            .addToBackStack(null)
            .commit()
    return true
}

For more information you can check out the Settings guide and the AndroidX Preference Sample


EDIT: a sample of the first solution, after updating, available here.

Here's how it can work (sample available here) :

MainActivity.kt

class MainActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
    override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
        //Note: this whole function won't be needed when using new version of fragment dependency (1.1.0 and above)
        val fragment = Fragment.instantiate(this, pref.fragment, pref.extras)
        fragment.setTargetFragment(caller, 0)
        supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack(null).commit()
        return true
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        if (savedInstanceState == null)
            supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
    }

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

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

preferences.xml

  <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">

    <Preference
      app:fragment="com.example.user.myapplication.MainActivity$PrefsFragment2" app:key="screen_preference" app:summary="Shows another screen of preferences"
      app:title="Screen preference"/>

  </PreferenceScreen>

preferences2.xml

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="Demo">

  <PreferenceCategory android:title="Category">
    <CheckBoxPreference
      android:key="next_screen_checkbox_preference" android:summary="AAAA" android:title="Toggle preference"/>
  </PreferenceCategory>

</PreferenceScreen>

gradle dependencies:

implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.0.0'
Neile answered 11/12, 2018 at 14:0 Comment(9)
I don't see onPreferenceStartFragment available anywhere. Can you please show how you got to use it? Also, the solution of 1.1.0-alpha01 seems to work well (without the need for onPreferenceStartFragment). But be careful when using this alpha version. It seems to cause crashes : issuetracker.google.com/issues/120687886Grith
Anyway, you got the answer right, so I've marked it as the correct one. Thank you.Grith
The method is part of this interface: developer.android.com/reference/androidx/preference/…Neile
Can you please show the full code, then ? Use mine, and just modify itGrith
Your code is fine, you are just implementing the wrong interface: PreferenceFragmentCompat.OnPreferenceStartScreenCallback and override fun onPreferenceStartScreen(caller: PreferenceFragmentCompat, pref: PreferenceScreen) Instead just rename this to: PreferenceFragmentCompat.OnPreferenceStartFragmentCallback and override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference) And it will work fineNeile
I don't seefragmentFactory. How did you call it? And my code of onPreferenceStartScreen had a call to create PrefsFragment2 directly instead of using what's on the Preference itself which tells which fragment to go to.Grith
Maybe as such: s000.tinyupload.com/?file_id=35271103952026251688 ?Grith
fragmentFactory is new in Fragment 1.1.0*, see the release notes. If you are not using Fragment 1.1.0*, then you should use (this is now deprecated in 1.1.0) this instead - similar to your changed code: final Fragment fragment = Fragment.instantiate(getContext(), preference.getFragment(), preference.getExtras());Neile
version Fragment 1.1.0 is quite problematic at the moment. I've tried what you wrote, and it seems to work very well. Gave you +1 for the effort. I decided to edit your answer to have the full solutionGrith

© 2022 - 2024 — McMap. All rights reserved.