Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 381
android developer Avatar asked Oct 09 '18 14:10

android developer


3 Answers

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'
like image 82
Louis Avatar answered Oct 16 '22 07:10

Louis


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.

like image 3
android developer Avatar answered Oct 16 '22 07:10

android developer


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.

like image 3
TwiXter Avatar answered Oct 16 '22 08:10

TwiXter