First question on stackoverflow, please forgive any inconsistencies.
My Android app implements a PreferenceFragmentCompat fragment which is opened through a button click in the MainActivity. All the options in "root" PreferenceScreen work smoothly but I cannot open any "child" PreferenceScreens.
After a lot of search, I found the need to implement the onPreferenceStartScreen callback in my fragment and it did work! But now, I made quite a few changes to the app and must have screwed something up and can't figure out what.
So here it goes!
Among others, I implement these 2 libraries in my app level gradle.build
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
This is my test pref3.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:defaultValue="false"
android:key="check_box_preference_1"
android:title="Check box preference" />
<PreferenceScreen android:title="Preference Screen">
<CheckBoxPreference
android:defaultValue="false"
android:key="check_box_preference_2"
android:title="Check box preference" />
</PreferenceScreen>
</PreferenceScreen>
This is my test java Preferences fragment (Common.log is my utility logger method)
public class TestPrefFrag extends PreferenceFragmentCompat implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
private static final String TAG = "TestPrefFrag";
#Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Common.log(5, TAG, "onCreatePreferences: started");
setPreferencesFromResource(R.xml.prefs3, rootKey);
}
#Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen preferenceScreen) {
Common.log(5, TAG, "onPreferenceStartScreen: '" + caller.getTag() + "' called for key '" + preferenceScreen.getKey() + "'");
caller.setPreferenceScreen(preferenceScreen);
return true;
}
#Override
public void onNavigateToScreen(PreferenceScreen preferenceScreen) {
Common.log(5, TAG, "onNavigateToScreen: called for key '" + preferenceScreen.getKey() + "'");
//getCallbackFragment();
super.onNavigateToScreen(preferenceScreen);
}
#Override
public boolean onPreferenceTreeClick(Preference preference) {
Common.log(5, TAG, "onPreferenceTreeClick: detected click # '" + preference.getKey() + "'");
return super.onPreferenceTreeClick(preference);
}
}
When I run this
clicking the CheckBoxPreference in the root screen calls the
onPreferenceTreeClick only
clicking the PreferenceScreen in the root screen calls the onNavigateToScreen and then the onPreferenceTreeClick but never the onPreferenceStartScreen
Shouldn't onPreferenceStartScreen be called right after onNavigateToScreen.
What am I doing wrong?
Thanks for any help!
You need to override the PreferenceFragment method getCallbackFragment, like this
#Override
public Fragment getCallbackFragment() {
return this;
}
Related
I'm on API level 22 and using a PreferenceFragmentCompat.
I have defined all the preferences in an xml file:
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:key="AUDIO0_PREAMP"
android:title="AUDIO0_PREAMP"/>
...
</androidx.preference.PreferenceScreen>
Then I want to listen to changes of these preferences:
public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
...
#Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.preferences, rootKey);
}
#Override
public void onResume() {
super.onResume();
getPreferenceScreen().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
#Override
public void onPause() {
super.onPause();
getPreferenceScreen().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
}
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
SwitchPreferenceCompat preference = (SwitchPreferenceCompat)findPreference(key);
Log.d(LOG_TAG, "onSharedPreferenceChanged: " + key + ", value: " + (preference.isChecked() ? "1" : "0"));
}
However when I change the preference on the UI, the onSharedPreferenceChanged gets called 2 times. First time with the correct value, second time with a 0 value. I'm wondering why this happens and how can I solve it?
Here is a logcat when I change the preference's value to TRUE:
D/SettingsFragment: onSharedPreferenceChanged: AUDIO0_PREAMP, value: 1
D/SettingsFragment: onSharedPreferenceChanged: AUDIO0_PREAMP, value: 0
And here is a logcat when I change it back to FALSE:
D/SettingsFragment: onSharedPreferenceChanged: AUDIO0_PREAMP, value: 0
D/SettingsFragment: onSharedPreferenceChanged: AUDIO0_PREAMP, value: 0
Now of course I can initialize a barrier to only process the first call to this method but it is not elegant and it will only hide the problem.
I want to know the exact cause of this, and the proper way of solving.
UPDATE1:
I change the preference value manually on the UI with my fingers - or on the emulator with the mouse. I have this issue on the real device and on emulator as well.
Thanks!
I have tried this, and it's working fine. Try with Preference category
Add Gradle dependency
implementation 'com.android.support:preference-v7:28.0.0'
Then preferences.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="Category 1">
<SwitchPreferenceCompat
android:key="AUDIO0_PREAMP"
android:defaultValue="true"
android:title="AUDIO0_PREAMP"/>
<SwitchPreferenceCompat
android:key="key1"
android:title="Switch Preference"
android:summary="Switch Summary"
android:defaultValue="true" />
<EditTextPreference
android:key="key2"
android:title="EditText Preference"
android:summary="EditText Summary"
android:dialogMessage="Dialog Message"
android:defaultValue="Default value" />
<CheckBoxPreference
android:key="key3"
android:title="CheckBox Preference"
android:summary="CheckBox Summary"
android:defaultValue="true"/>
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
Then Fragment
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
/**
* A simple {#link Fragment} subclass.
*/
public class FragmentPreferences extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
#Override
public void onResume() {
super.onResume();
getPreferenceScreen().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
#Override
public void onPause() {
super.onPause();
getPreferenceScreen().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
}
#Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.preferences,rootKey);
}
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
SwitchPreferenceCompat preference = (SwitchPreferenceCompat)findPreference(key);
Log.d("PreferenceChanged", "onSharedPreferenceChanged: " + key + ", value: " + (preference.isChecked() ? "1" : "0"));
}
}
Try Above, if that's not working then post comment
I'm trying to learn ways to build preference pages in the Xamarin Android application.
I found a lot of examples with PreferenceFragment but it was marked as deprecated and it is difficult for me to rewrite them on the current stage.
I've created activity to represent headers. I added IntentFilter so I can access this activity from apps list in the settings menu. Also it has internal class to group some preferences together:
namespace droid.examples.Preferences
{
[Activity(Label = "Settings activity", Theme = "#style/AppTheme", Name = "droid.examples.Preferences.SettingsActivity")]
[IntentFilter(new string[] { "android.intent.action.APPLICATION_PREFERENCES" })]
public class SettingsActivity : PreferenceActivity
{
public override void OnBuildHeaders(IList<Header> target)
{
base.OnBuildHeaders(target);
LoadHeadersFromResource(Resource.Xml.preference_headers, target);
}
public class SettingsFragment : PreferenceFragmentCompat
{
public override void OnCreatePreferences(Bundle savedInstanceState, string rootKey)
{
// Load the Preferences from the XML file
SetPreferencesFromResource(Resource.Xml.app_preferences, rootKey);
}
}
}
}
My app_preferences.xml which I can't open by selecting "Prefs 1" header from preference_headers.xml:
<?xml version="1.0" encoding="utf-8" ?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Category">
<CheckBoxPreference
android:key="checkbox_preference"
android:title="Developer mode"
android:summary="Allow user to see detailed messages" />
</PreferenceCategory>
</PreferenceScreen>
I have preference_headers.xml. It opens when I click on gear wheel near application name. It looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="droid.examples.Preferences.SettingsActivity.SettingsFragment"
android:title="Prefs 1"
android:summary="An example of some preferences." />
</preference-headers>
My package name: droid.examples
I think that one problem related to the android:fragment attribute value.
What is the rules to build that value?
I suppose that it must start from 'package name'. Should it contain namespace between class name and package name?
What does $ mean in the attribute value? Is it used to mark internal class? I saw in the several places next code:
android:fragment="com.example.android.apis.preference.PreferenceWithHeaders$Prefs1Fragment"
I hope you can help me find where I made a mistakes.
Source code from GitHub
I spend a lot of time to investigate that issue and I want to make a summary.
We have to override IsValidFragment method in the SettingsActivity:
protected override bool IsValidFragment(string fragmentName)
{
return fragmentName == "droid.examples.preferences.SettingsActivity.SettingsFragment";
}
My SettingsActivity extends PreferenceActivity. Thanks to #Jeremy advice about implementation of IOnPreferenceStartFragmentCallback I find out that base class already extends it.
public abstract class PreferenceActivity ...
{
...
public virtual bool OnPreferenceStartFragment(PreferenceFragment caller, Preference pref);
...
}
So, I probably need to use PreferenceFragment instead of PreferenceFragmentCompat to make code consistent:
public class SettingsFragment : PreferenceFragment
{
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
AddPreferencesFromResource(Resource.Xml.app_preferences_for_header);
}
}
Also we have to add Register attribute to our fragment:
[Register("droid.examples.preferences.SettingsActivity.SettingsFragment")]
public class SettingsFragment : PreferenceFragment
{
}
Finally I updated preference_headers.xml
<?xml version="1.0" encoding="utf-8" ?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="droid.examples.preferences.SettingsActivity.SettingsFragment"
android:title="Prefs 1"
android:summary="An example of some preferences." />
</preference-headers>
android:fragment attribute value can contains '$' but '+' won't work because Register doesn't support it and we will get compilation error.
Thanks everyone who tried to help me
Looks like the string you provide is just a message to send to your parent activity. Your parent activity is responsible for instantiating the right Fragment, and performing the ceremony to render it.
The platform docs seem to indicate as such:
When a user taps a Preference with an associated Fragment, the interface method PreferenceFragmentCompat.OnPreferenceStartFragmentCallback.onPreferenceStartFragment() is called
At time of writing, there's a code snippet on that page, which I've translated for my own project more-or-less as follows:
// This has to go in the activity which hosts the PreferenceFragment
// which owns the Preference that has the `android:fragment` attribute.
using Android.Support.V7.Preferences;
using Android.Support.V4.App;
partial class MyActivity :
PreferenceFragmentCompat.IOnPreferenceStartFragmentCallback
{
Fragment GetFragmentForPrefString(string prefFragment)
{
// you implement this
}
const int fragmentContainerId = Resource.Id.whatever;
public bool OnPreferenceStartFragment(
PreferenceFragmentCompat caller, Preference pref)
{
string prefString = pref.Fragment;
var transaction = SupportFragmentManager.BeginTransaction();
transaction.Replace(fragmentContainerId,
GetFragmentForPrefString(prefString));
// you'll probably also want to add it to the back stack,
// but it's not strictly necessary I guess.
transaction.Commit();
return true;
}
}
Their sample involves the Java API method getSupportFragmentManager().getFragmentFactory() which doesn't appear to be part of the V28 Xamarin support NuGet packages. But honestly I'm not sure why that level of indirection is necessary; I'd suggest you simply implement something like
switch (prefFragmentName)
{
case "Fragment 1":
return new Fragment1();
// etc
As I was following an old tutorial (Créez des applications pour Android -> openclassroom) I got stuck on this deprecated method addPreferencesFromResource(int id) from the PreferenceActivity class.
So my question is :
What is the new way of creating Preferences in Android ?
I found this post (What to use instead of “addPreferencesFromResource” in a PreferenceActivity?) that help me understand that you have to go through a PreferenceFragment in order to do it.
In the following explanation I use your.package. just to show that you have to put the package name. Everybody has its own package so please replace it with your package.
lets begin :
1. Preference Fragment
Create your PreferenceFragment class
MyPreferenceFragment
public class MyPreferenceFragment extends PreferenceFragment
{
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.fragment_preference);
}
}
Then the associated xml resource
fragment_preference.xml (in the folder res/xml of your project)
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="FOO">
<CheckBoxPreference
android:key="checkBoxPref"
android:title="check it out"
android:summary="click this little box"/>
</PreferenceCategory>
</PreferenceScreen>
That's all for the Fragment part.
2. Preference Activity
Create the PreferenceActivity class
MyPreferenceActivity
public class MyPreferenceActivity extends PreferenceActivity
{
#Override
public void onBuildHeaders(List<Header> target)
{
loadHeadersFromResource(R.xml.headers_preference, target);
}
#Override
protected boolean isValidFragment(String fragmentName)
{
return MyPreferenceFragment.class.getName().equals(fragmentName);
}
}
Do not forget to override isValidFragment(String fragmentName) method as you will get punched in the face by your application ! ;) More seriously I have no idea why you need to do this but it is needed. If someone has an explanation about this I'd gladly read it :)
EDIT :
Thanks to kirtan403 I now know why it is needed : it has to be set because of an (android framework fragment injection).
As you can see in the onBuildHeaders(List<Header> target) we load another xml file that contain the headers of the preference. In short, headers are the left part of the preference and the fragment are the right part (for tablet). For a phone you will first have the headers and when you click on an item the corresponding fragment will be put on top of the headers list.
Read this article (Multi-pane development in Android with Fragments - Tutorial) the images explain themselves.
Then the associated xml resource
headers_preference.xml (in the folder res/xml of your project)
<?xml version="1.0" encoding="utf-8"?>
<preference-headers
xmlns:android="http://schemas.android.com/apk/res/android">
<header
android:fragment="your.package.MyPreferenceFragment"
android:title="Goto: Preference fragment"
android:summary="An example of some preferences." />
</preference-headers>
As you may have noticed in the header section you have :
android:fragment="your.package.MyPreferenceFragment"
This will act as a Link to the fragment you want to show. On Tablet it will load on the right part and on the phone it will load on top of the current view.
3. Android Manifest
Now what you should do is to add your Activity to the AndroidManifest.xml file.
Inside the application section add these lines :
<activity
android:name="your.package.MyPreferenceActivity"
android:label="Preferences">
</activity>
You will probably tell me :
"Oh darling you forgot to put android:launchMode="singleTask" in your actvity"
But DO NOT PUT THIS as you will never load your fragment on phone. This error was solved by a great man ! This is the link to his blog (Android header preferences on small screen/phone).
4. Start the Preferences from Menu
Finally you need to add the ability to show this Preference !! To do so you will need 3 things :
The Menu
menu.xml (in the folder res/menu of your project)
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/preferences"
android:title="Preferences" />
</menu>
Loading this Menu in your Main activity (not the PreferenceActivity) under the method onCreateOptionsMenu(Menu menu)
#Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu, menu);
return true;
}
Starting the MyPreferenceActivity Activity when you click on that button.
For that you will need to override the onOptionsItemSelected(MenuItem item) method in your Main activity.
#Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch(item.getItemId())
{
case R.id.preferences:
{
Intent intent = new Intent();
intent.setClassName(this, "your.package.MyPreferenceActivity");
startActivity(intent);
return true;
}
}
return super.onOptionsItemSelected(item);
}
Et voila les amis !
I haven't tested this code. I took it and modified it from my own code so I may have not well copy pasted things. If you encounter errors tell me, I'll try to figure out the problem and fix this.
I hope this post will help some people out there :D
Cheers !
I liked the solution from this post: http://alvinalexander.com/android/android-tutorial-preferencescreen-preferenceactivity-preferencefragment
.. because it seems the most compact for someone that just needs something very basic up and running quickly. It has only one .java file and two small xml files.
Activity Config REMINDERS
After adding the 3 files to your project, Don't forget to
A) Add the Prefs Activity to Manifest file
B) Add some way to launch the Prefs Activity .. e.g., a Button or Menu item
Add the following files to your project. Use the order they are listed in to avoid compile errors.
Add /res/values/array.xml
<resources>
<string-array name="listArray">
<item>Ace</item>
<item>Club</item>
</string-array>
<string-array name="listValues">
<item>Ace</item>
<item>Club</item>
</string-array>
</resources>
Add /res/xml/preferences.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<EditTextPreference android:title="Your Name"
android:key="username"
android:summary="Please provide your username"></EditTextPreference>
<CheckBoxPreference android:title="Application Updates"
android:defaultValue="false"
android:summary="This option if selected will allow the application to check for latest versions."
android:key="applicationUpdates" />
<ListPreference android:title="Download Details"
android:summary="Select the kind of data that you would like to download"
android:key="downloadType"
android:defaultValue="Ace"
android:entries="#array/listArray"
android:entryValues="#array/listValues" />
</PreferenceScreen>
Add the Activity code
public class AppPreferenceActivity extends PreferenceActivity
{
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction().replace(android.R.id.content, new MyPreferenceFragment()).commit();
checkValues();
}
public static class MyPreferenceFragment extends PreferenceFragment
{
#Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
private void checkValues()
{
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
String strUserName = sharedPrefs.getString("username", "NA");
boolean bAppUpdates = sharedPrefs.getBoolean("applicationUpdates",false);
String downloadType = sharedPrefs.getString("downloadType","1");
String msg = "Cur Values: ";
msg += "\n userName = " + strUserName;
msg += "\n bAppUpdates = " + bAppUpdates;
msg += "\n downloadType = " + downloadType;
Toaster.shortDebug(msg);
}
}
I'm using the code below to print text to logcat, but the setOnPreferenceClickListener function doesn't catch the event.
I'm using Android API Level 8 to test the code.
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<CheckBoxPreference
android:defaultValue="true"
android:key="settings_use_cache"
android:summary="Use cache"
android:title="Use Cache" />
<Preference
android:defaultValue="true"
android:key="settings_delete_cache"
android:summary="Delete all cache data"
android:title="Clear Cache" />
</PreferenceScreen>
Here is the code
public static class CachePreferenceFragment extends
PreferenceActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
Log.w("DBG", "Oncreate started");
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_cache);
Preference settings_delete_cache=findPreference("settings_delete_cache");
settings_delete_cache.setOnPreferenceClickListener(new OnPreferenceClickListener() {
#Override
public boolean onPreferenceClick(Preference preference) {
Log.w("Prefence", "Deleting Cache");
return false;
}
});
}
}
What can I do to ensure the listener catches the event?
EDIT:
After talking with this dev outside of the thread, it was identified that the problem was that he was putting his listener inside of the fragment. So this code actually works, but it will only run if you are using a two-panel layout. In fact this is stated right above the method:
/**
* This fragment shows notification preferences only. It is used when the
* activity is showing a two-pane settings UI.
*/
public static class CachePreferenceFragment extends
PreferenceActivity {
The solution is to also set the onPreferenceClick listener in setupSimplePreferencesScreen()!
In my application, I used PreferenceFragment to create a nice application on Tablets and smartphones.
So, in my main activity, I use:
#Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_headers, target);
}
My xml file looks like this:
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="tof.cv.mpp.view.StockPreferenceFragment"
android:title="#string/btn_home_settings" android:summary="">
<extra android:name="resource" android:value="activity_preferences" />
</header>
<header android:fragment="tof.cv.mpp.view.StockPreferenceFragment"
android:title="#string/btn_home_planner" android:summary="">
<extra android:name="resource" android:value="activity_planner_preferences" />
</header>
<header android:fragment="tof.cv.mpp.view.StockPreferenceFragment"
android:title="#string/btn_home_twitter" android:summary="">
<extra android:name="resource" android:value="activity_twitter_preferences" />
</header>
</preference-headers>
The problem is now shen I want to use a OnSharedPreferenceChangeListener in order to update the summary of some of my preferences.
I use:
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
Log.i("", "PREFChanged "+getActivity());
if (key.contentEquals("prefPseudo")) {
Log.i("", "PseudoChanged");
Preference pref = findPreference("prefPseudo");
pref.setSummary(((EditTextPreference) pref).getText());
}
if (key.contentEquals(getString(R.string.key_activity))) {
Log.i("", "FirstChanged");
Preference pref = findPreference(getString(R.string.key_activity));
pref.setSummary(((ListPreference) pref).getEntry());
}
if (key.contentEquals(getString(R.string.key_planner_da))) {
Preference pref = findPreference(getString(R.string.key_planner_da));
Log.i("", "PlannerChanged"+pref);
pref.setSummary(((ListPreference) pref).getEntry());
}
}
The big problem I am facing is that getActivity() is null when I have multiple categories in my xml header!
The first one I open is always correct and non null, but when I press back, I come back to the Preference list automatically generated, I click on a second one and there, activity is null!
OUCH! That was an hard one.
I succeed to fix that.
In fact, the Listener was always belonging to the first Fragment.
So when you change the Preference from an other category, the listener of the first Fragment is called when you change a Preference of the second Fragment!
So the Activity is null.
The solution is to remove the listener when you leave a Fragment, so the correct Listener can do his job:
#Override
public void onPause() {
super.onPause();
Log.i("", "PAUSE");
preferences.unregisterOnSharedPreferenceChangeListener(this);
}
If your Listener is in a Fragment MyFragment use the following code to get the activity instance. We need to use .this.getActivity()
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
Log.i("", "PREFChanged "+ MyFragment.this.getActivity());
.....
}