I'm trying to create a custom Preference to be shown in PreferenceFragment as described here: Building a Custom Preference. My custom Preference should look and function as SwitchPreference, but have one additional TextView
for error reporting.
I got everything implemented and UI looks fine, but I can't initialize this Preference when my PreferenceFragment is shown!
Documentation for Preference.onBindView()
states that:
This is a good place to grab references to custom Views in the layout and set properties on them.
So I did:
@Override
protected void onBindView(View view) {
super.onBindView(view);
txtError = (TextView) view.findViewById(R.id.error);
}
public void setError(String errorMessage) {
txtError.setText(errorMessage);
notifyChanged();
}
However, when I call CustomSwitchPreference.setError(String)
in PreferenceFragment.onResume()
, I get NPE because txtError
is null.
I tried to find some workaround, but it looks like there is no lifecycle method in PreferenceFragment which is guaranteed to be called AFTER all the underlying Preferences
had their Views
initialized (I checked both Preference.onBindView(View)
and Preference.onCreateView(ViewGroup)
).
This behavior doesn't make any sense - there should be some way to initialize UIs of the underlying Preferences
when PreferenceFragment
is shown. How can I achieve this?
Note: calls to customPreference.setTitle(String)
and customPreference.setSummary(String()
in CustomPreferenceFragment.onResume()
work fine. It is just the additional TextView
which I can't grab a reference to...
CustomSwitchPreference.java:
public class CustomSwitchPreference extends SwitchPreference {
private TextView txtError;
public CustomSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public CustomSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomSwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomSwitchPreference(Context context) {
super(context);
}
@Override
protected View onCreateView(ViewGroup parent) {
setLayoutResource(R.layout.custom_switch_preference_layout);
return super.onCreateView(parent);
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
txtError = (TextView) view.findViewById(R.id.error);
}
public void setError(String errorMessage) {
txtError.setText(errorMessage);
notifyChanged();
}
}
CustomPreferenceFragment.java:
public class CustomPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getPreferenceManager().setSharedPreferencesName(PREFERENCES_FILE_NAME);
addPreferencesFromResource(R.xml.application_settings);
}
@Override
public void onResume() {
super.onResume();
Preference preference = findPreference("CUSTOM_PREF");
if (preference == null ||
!CustomSwitchPreference.class.isAssignableFrom(preference.getClass()))
throw new RuntimeException("couldn't get a valid reference to custom preference");
CustomSwitchPreference customPreference = (CustomSwitchPreference) preference;
customPreference.setError("error");
}
}
custom_switch_preference_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@android:id/widget_frame">
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"/>
<TextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="3"/>
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="3"/>
</LinearLayout>
<FrameLayout
android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"/>
</RelativeLayout>
application_settings.xml:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<com.example.settings.CustomSwitchPreference
android:key="CUSTOM_PREF"/>
</PreferenceScreen>
I couldn't find a proper solution for this issue - this inconsistency feels like a life-cycle bug in AOSP, but I'm not 100% sure about this.
As a workaround, I defined a callback interface that CustomSwitchPreference
invokes in onBindView
method in order to notify the containing PreferenceFragment
that it had been initialized:
@Override
protected void onBindView(View view) {
super.onBindView(view);
txtError = (TextView) view.findViewById(R.id.error);
initializationListener.onInitialized(CustomSwitchPreference.this);
}
and all the manipulations on this CustomSwitchPreference
that I wanted to perform in onResume
now get performed in onInitialized
callback. This is an ugly workaround that requires a considerable amount of boilerplate, but it seems to work.