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) .
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 :
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.
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) :
Set a "android:fragment" for the PreferenceScreen
, to point to the new fragments classes, similar to headers. This didn't do anything at all.
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" .
Tried to avoid using ARG_PREFERENCE_ROOT , but had same crash as on #2 .
As suggested here, I tried to return this
in function getCallbackFragment
, but this didn't help at all.
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?
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'
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.
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...
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.
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With