Android Settings Screen with PreferenceFragmentCompat: Kotlin XML Guide

Settings screens are an essential component of most Android applications. They allow users to customize the app according to their preferences. The PreferenceFragmentCompat class, part of the AndroidX Preference library, provides a robust and straightforward way to build settings screens using XML for layout definition and Kotlin for logic implementation.

What is PreferenceFragmentCompat?

PreferenceFragmentCompat is a class from the AndroidX Preference library designed to display preferences as a hierarchy of Preference objects. These preferences can be stored using SharedPreferences and are automatically updated when the user changes settings.

Why Use PreferenceFragmentCompat?

  • Simple and Structured: Organizes settings into a structured, user-friendly interface.
  • Automatic Storage: Utilizes SharedPreferences to automatically store and retrieve user settings.
  • Customizable: Provides various types of preferences (checkboxes, lists, switches, etc.) that can be customized via XML.
  • Compatibility: Part of the AndroidX library, ensuring backward compatibility and consistent behavior across different Android versions.

Step-by-Step Implementation

Let’s walk through building a settings screen using PreferenceFragmentCompat with XML layouts and Kotlin code.

Step 1: Add Dependencies

First, ensure that you have the necessary dependencies in your build.gradle file:

dependencies {
    implementation("androidx.preference:preference-ktx:1.2.1")
}

Step 2: Create a Preference XML File

Create an XML file that defines the structure and content of your settings. For example, preferences.xml inside the res/xml directory:

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory app:title="User Profile">

        <EditTextPreference
            app:key="username"
            app:title="Username"
            app:summary="Enter your username"
            app:defaultValue="Guest"
            app:useSimpleSummaryProvider="true"/>

        <EditTextPreference
            app:key="email"
            app:title="Email"
            app:summary="Enter your email address"
            app:defaultValue="guest@example.com"
            app:useSimpleSummaryProvider="true"
            app:inputType="textEmailAddress"/>

    </PreferenceCategory>

    <PreferenceCategory app:title="Notifications">

        <SwitchPreferenceCompat
            app:key="enable_notifications"
            app:title="Enable Notifications"
            app:summaryOn="Notifications are enabled"
            app:summaryOff="Notifications are disabled"
            app:defaultValue="true"/>

    </PreferenceCategory>

    <PreferenceCategory app:title="Advanced Settings">

        <ListPreference
            app:key="theme"
            app:title="Theme"
            app:summary="Select your preferred theme"
            app:entries="@array/theme_entries"
            app:entryValues="@array/theme_values"
            app:defaultValue="light"
            app:useSimpleSummaryProvider="true"/>

    </PreferenceCategory>

</PreferenceScreen>

Create the arrays theme_entries and theme_values in res/values/arrays.xml:

<resources>
    <string-array name="theme_entries">
        <item>Light</item>
        <item>Dark</item>
        <item>System Default</item>
    </string-array>

    <string-array name="theme_values">
        <item>light</item>
        <item>dark</item>
        <item>system</item>
    </string-array>
</resources>

Step 3: Create a Settings Fragment

Create a Kotlin class that extends PreferenceFragmentCompat and inflates the preference XML:

import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat

class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.preferences, rootKey)
    }
}

Step 4: Integrate the Settings Fragment into an Activity

Add the settings fragment to your activity. For instance, you can create a SettingsActivity:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.commit

class SettingsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        setSupportActionBar(findViewById(R.id.toolbar))
        supportActionBar?.setDisplayHomeAsUpEnabled(true)

        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                replace(R.id.settings_container, SettingsFragment())
            }
        }
    }
}

Create a layout file (e.g., activity_settings.xml) for the activity that includes a FrameLayout to host the settings fragment and a toolbar:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

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

</LinearLayout>

Step 5: Add Theme Attribute to Activity in AndroidManifest.xml

Add android:theme="@style/Theme.AppCompat.Light" to the activity tag inside your AndroidManifest.xml:

<activity
    android:name=".SettingsActivity"
    android:exported="false"
    android:label="Settings"
    android:theme="@style/Theme.AppCompat.Light">
    <meta-data
        android:name="android.app.lib_name"
        android:value="" />
</activity>

Step 6: Accessing Preference Values

You can access the stored preference values using SharedPreferences:

import android.content.Context
import androidx.preference.PreferenceManager

fun getUsername(context: Context): String {
    val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
    return sharedPreferences.getString("username", "Guest") ?: "Guest"
}

Handling Preference Changes

To react to preference changes, implement the SharedPreferences.OnSharedPreferenceChangeListener in your activity or application context. For example:

import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager

class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        PreferenceManager.getDefaultSharedPreferences(this)
            .registerOnSharedPreferenceChangeListener(this)
    }

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
        key?.let {
            when (it) {
                "enable_notifications" -> {
                    val notificationsEnabled = sharedPreferences?.getBoolean(it, true) ?: true
                    Log.d("Settings", "Notifications enabled: $notificationsEnabled")
                }
                "theme" -> {
                    val theme = sharedPreferences?.getString(it, "light") ?: "light"
                    Log.d("Settings", "Theme changed to: $theme")
                    // Apply theme here
                }
                else -> {
                    Log.d("Settings", "Preference changed: $it")
                }
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        PreferenceManager.getDefaultSharedPreferences(this)
            .unregisterOnSharedPreferenceChangeListener(this)
    }
}

Customizing Preference Appearance

You can customize the appearance of your preferences by defining custom layouts for individual preference types. For example:

<androidx.preference.Preference
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout="@layout/custom_preference_layout"
    app:iconSpaceReserved="false"/>

Conclusion

Using PreferenceFragmentCompat in Kotlin with XML-based layouts is a straightforward and effective way to create customizable settings screens in Android applications. By following the steps outlined above, you can quickly build, integrate, and manage application settings while providing users with a seamless and personalized experience. This approach ensures a clean, structured, and maintainable codebase, making it an excellent choice for modern Android development.