21

I need a custom layout for my PreferenceFragmentCompat. In the docs for PreferenceFragmentCompat it seems that you can possibly inflate and return a view in onCreateView().

However a NPE results:-

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.widget.RecyclerView.setAdapter(android.support.v7.widget.RecyclerView$Adapter)' on a null object reference
                                                                       at android.support.v7.preference.PreferenceFragmentCompat.bindPreferences(PreferenceFragmentCompat.java:511)
                                                                       at android.support.v7.preference.PreferenceFragmentCompat.onActivityCreated(PreferenceFragmentCompat.java:316)
                                                                       at com.cls.example.MyPrefFrag.onActivityCreated(MyPrefFrag.java:42) 

After I checked the source of PreferenceFragmentCompat:onCreateView I found the following piece of code :-

 RecyclerView listView = this.onCreateRecyclerView(themedInflater, listContainer, savedInstanceState);
 if(listView == null) {
    throw new RuntimeException("Could not create RecyclerView");
 } else {
    this.mList = listView;   //problem
    ...
    return view;
 }

So if you override onCreateView() and return a custom layout the onCreateRecyclerView() is not called plus the RecyclerView private field mList will not be set. So the NPE on setAdapter() results.

Should I assume that having a custom layout is not feasible for PreferenceFragmentCompat ?

Lakshman Chilukuri
  • 1,067
  • 2
  • 11
  • 20
  • PreferenceFragment(Compat) was designed to provide a standard UI for settings. May I ask, why do you want to use a custom layout? Anyways, you can use a custom layout with custom preferences handling if you really need that. – Gergely Kőrössy Mar 15 '16 at 08:38
  • I got a custom view at the beginning of the custom layout which changes with each preference change. With native PreferenceFragment it is easy. You need to just embed a listview with id=android:id/list in your custom layout. PreferencefragmentCompat does not seem to have a similar functionality. – Lakshman Chilukuri Mar 15 '16 at 09:14
  • You can do the same with overriding [`onCreateRecyclerView(...)`](http://developer.android.com/reference/android/support/v7/preference/PreferenceFragmentCompat.html#onCreateRecyclerView%28android.view.LayoutInflater,%20android.view.ViewGroup,%20android.os.Bundle%29) which should return the RecyclerView from your layout. – Gergely Kőrössy Mar 15 '16 at 09:40
  • That does not serve my requirement. The recycler view is used for the preferences. I need a custom layout. – Lakshman Chilukuri Mar 15 '16 at 11:45
  • Then you have to implement it your own way. The `PreferenceFragmentCompat` is for the standard layout, it does nothing special behind the scenes so it shouldn't be a problem implementing your custom settings UI with custom business logic using the classic `SharedPreferences`. – Gergely Kőrössy Mar 15 '16 at 11:48
  • Yes. But IMO its hardly a trivial task. – Lakshman Chilukuri Mar 15 '16 at 12:47
  • TBH I don't see the difference between `PreferenceFragment` with a `ListView` and `PreferenceFragmentCompat` with a `RecyclerView`. Both fill the given layouts with the preference layouts loaded according to the XML supplied as your preference list. If you just need custom list items, you can customize the individual layouts by overriding the style elements inside the PreferenceThemeOverlay (use CTRL + click in your IDE or check out the sources on the Internet to explore possible values). – Gergely Kőrössy Mar 15 '16 at 13:15

3 Answers3

26

You can specify a custom layout in your theme.

For example:

styles.xml

<style name="YourTheme" parent="Theme.AppCompat">
    <!-- ... -->
    <item name="preferenceTheme">@style/YourTheme.PreferenceThemeOverlay</item>
</style>

<style name="YourTheme.PreferenceThemeOverlay" parent="@style/PreferenceThemeOverlay">
    <item name="android:layout">@layout/fragment_your_preferences</item>
</style>

fragment_your_preferences.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/custom_toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </android.support.design.widget.AppBarLayout>

    <!-- Required ViewGroup for PreferenceFragmentCompat -->
    <FrameLayout
        android:id="@android:id/list_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    </FrameLayout>

</LinearLayout>

And then in onViewCreated() of your fragment class you can start using the views:

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState)
{
    super.onViewCreated(view, savedInstanceState);

    Toolbar toolbar = (Toolbar)view.findViewById(R.id.custom_toolbar);

    if (toolbar != null)
    {
        getMainActivity().setSupportActionBar(toolbar);
    }
}
Trevor Balcom
  • 3,766
  • 2
  • 32
  • 51
ByteWelder
  • 5,464
  • 1
  • 38
  • 45
  • I did try the custom layout with just a textview and list_container viewgroup in the theme. It did not work. The preferences did not get populated. Additionally on scrolling multiple textviews got scrolled. To further troubleshoot I tried making the custom layout exactly same as the library layout for preferencefragmentcompat and tried. Again the preferences did not get populated. Did this code work for you ? – Lakshman Chilukuri Mar 30 '16 at 02:49
  • Did you add the preferences in onCreate()? `@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); }` And are you using `PreferenceFragmentCompat`? – ByteWelder Mar 30 '16 at 08:42
  • No I added it in onCreatePreferences(). I dont know why I did that. I will add it in onCreate() and revert. I had given up and gone back to native fragments. – Lakshman Chilukuri Mar 30 '16 at 10:59
  • 3
    You're welcome! If in the future you run into a similar problem, you can open the class that gives you the problem like `PreferenceFragmentCompat.class` in Android Studio. When you open the class file, it should show you (decompiled) readable Java code. If you don't see Java code, install the `Java Bytecode Decompiler` plugin in the Android Studio settings. After that you can start digging around where the issue is: in this case, you can see that `PreferenceFragmentCompat.onCreateView` has a stylable layout and looks for `R.id.listContainer`. – ByteWelder Mar 30 '16 at 12:24
  • I tried same as you told me. but it didn't worked for me. – Deepak Rajput Jan 10 '20 at 03:53
  • Why is the framelayout required for PreferenceFragmentCompat? Documentation ref? I am unable to display such PreferenceFragmentCompat, hence the question. Also valid for AndroidX? Thanks – carl May 18 '20 at 13:03
  • If you have multiple settings screens, you are basically doomed. Love the cr*ppy and useless solutions from Google – Farid Apr 15 '22 at 18:13
3

I had the same problem, however my requirements were a bit different, I just needed to show a layout above the settings list, so I did this

<NestedScrollView>
  <ConstrainLayout>
    <CustomView>
    <SettingsFragmentHolder/>
  </ConstraintLayout>
</NestedScrollView>

Then later in code I just do this

FragmentManager.beginTransaction().replace(holder, PreferencesFragment()).commit()

One thing to note is that, preferences fragment has its own scrollview or a list, that will cause the NestedScrollView to scroll to the beginning of PreferencesFragment layout, to fix that add this to the parent layout of the PreferencesFragment, in this case it is ConstraintLayout

android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"

That will stop the default scrolling behavior

abdu
  • 667
  • 5
  • 14
0

You can. In my example I specified custom layout:

preferences_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:text="Custom Layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

    <FrameLayout
        android:id="@+id/settings_container_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />

    <TextView
        android:text="End of Layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

</LinearLayout>

I created class it extends PreferenceFragmentCompat. You must override methods:

onCreatePreferences

@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
    setPreferencesFromResource(R.xml.app_main_preferences, rootKey);
}

This method sets your preferences. In my Example I use app_main_preferences.xml

app_main_preferences.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">

    <Preference
        app:key="logout"
        app:title="Logout"
        app:summary="Logout From The Application Account"/>

</PreferenceScreen>

onCreateView

@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View layout = inflater.inflate(R.layout.preferences_fragment, container, false);
        ViewGroup settingsContainerView = layout.findViewById(R.id.settings_container_view);

        Toolbar toolbar = layout.findViewById(R.id.toolbar);

        MainActivity mainActivity = (MainActivity) requireActivity();
        mainActivity.setSupportActionBar(toolbar);

        View settingsView = super.onCreateView(inflater, settingsContainerView, savedInstanceState);
        settingsContainerView.addView(settingsView);
        return layout;
}

Congratulations!

enter image description here

How it works:

In our custom layout we create FrameLayout it's our container for settings, Toolbar and TextView widgets.

Then we create class it extends PreferenceFragmentCompat class. We override methods. In onCreatePreferences method we set our setting's layout using setPreferencesFromResource.

And our "Main" method is onCreateView method.

View layout = inflater.inflate(R.layout.preferences_fragment, container, false);

We inflate our custom layout using our inflater and it inflate method, we pass our custom layout and other parameters such container and savedInstanceBundle.

ViewGroup settingsContainerView = layout.findViewById(R.id.settings_container_view);

Toolbar toolbar = layout.findViewById(R.id.toolbar);

MainActivity mainActivity = (MainActivity) requireActivity();

We get our FrameLayout (it's container for our settings), toolbar and MainActivity of our application.

mainActivity.setSupportActionBar(toolbar);

We set our toolbar using setSupportActionBar method.

View settingsView = super.onCreateView(inflater, settingsContainerView, savedInstanceState);
settingsContainerView.addView(settingsView);
return layout;

Finally we call super.onCreateView method which populate settings and returns populated view. We must add our populated view as child to our settingsContainerView using addView method and return layout variable.

aandrosov
  • 1
  • 2