Creating Dynamic Themes with User Input in XML

In Android development, themes play a crucial role in defining the visual appearance of your app. While static themes are easy to implement, dynamic themes that adapt to user preferences offer a more personalized and engaging user experience. This article delves into how you can create dynamic themes in Android using XML, allowing users to customize the look and feel of your application without requiring code modifications.

Understanding Themes in Android

Themes in Android are a set of attributes that define the look and feel of an application. These attributes can include colors, text styles, and other visual properties. By default, themes are defined in XML files within the res/values/ directory.

Why Dynamic Themes?

Dynamic themes offer several advantages:

  • User Customization: Allows users to personalize their app experience.
  • Accessibility: Enables users to choose themes that are easier on their eyes (e.g., dark mode).
  • Branding: Provides an option to switch between different branding styles based on user preferences.

Implementing Dynamic Themes Using XML

Here’s a step-by-step guide on how to create dynamic themes that respond to user input in your Android application using XML.

Step 1: Define Themes in XML

First, create multiple theme files in the res/values/ directory. Each file will represent a different theme. For example, themes_light.xml and themes_dark.xml.

themes_light.xml
<resources>
    <style name="LightTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
        <item name="colorPrimary">@color/light_primary</item>
        <item name="colorPrimaryVariant">@color/light_primary_variant</item>
        <item name="colorSecondary">@color/light_secondary</item>
        <item name="android:colorBackground">@color/light_background</item>
        <item name="android:textColor">@color/light_text</item>
    </style>
</resources>
themes_dark.xml
<resources>
    <style name="DarkTheme" parent="Theme.MaterialComponents.Dark.NoActionBar">
        <item name="colorPrimary">@color/dark_primary</item>
        <item name="colorPrimaryVariant">@color/dark_primary_variant</item>
        <item name="colorSecondary">@color/dark_secondary</item>
        <item name="android:colorBackground">@color/dark_background</item>
        <item name="android:textColor">@color/dark_text</item>
    </style>
</resources>

Define the color values in separate colors.xml files for both themes.

colors_light.xml
<resources>
    <color name="light_primary">#6200EE</color>
    <color name="light_primary_variant">#3700B3</color>
    <color name="light_secondary">#03DAC5</color>
    <color name="light_background">#FFFFFF</color>
    <color name="light_text">#000000</color>
</resources>
colors_dark.xml
<resources>
    <color name="dark_primary">#BB86FC</color>
    <color name="dark_primary_variant">#3700B3</color>
    <color name="dark_secondary">#03DAC6</color>
    <color name="dark_background">#121212</color>
    <color name="dark_text">#FFFFFF</color>
</resources>

Step 2: Create a Theme Switching Mechanism

To allow users to switch themes, you’ll need a UI element, such as a Switch or a RadioButton. Implement the logic to detect user input and apply the selected theme.

First, add the necessary UI elements to your layout XML.

activity_main.xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sample Text"
        android:textSize="20sp"
        android:layout_marginBottom="16dp"/>

    <Switch
        android:id="@+id/themeSwitch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Dark Theme" />

</LinearLayout>

In your MainActivity, implement the logic to handle the Switch state and apply the theme accordingly.

MainActivity.kt
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.Switch
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate

class MainActivity : AppCompatActivity() {

    private lateinit var themeSwitch: Switch
    private lateinit var textView: TextView
    private lateinit var sharedPreferences: SharedPreferences

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

        themeSwitch = findViewById(R.id.themeSwitch)
        textView = findViewById(R.id.textView)
        sharedPreferences = getSharedPreferences("themePref", Context.MODE_PRIVATE)

        // Load saved theme
        val isDarkTheme = sharedPreferences.getBoolean("isDarkTheme", false)
        themeSwitch.isChecked = isDarkTheme
        setAppTheme(isDarkTheme)

        themeSwitch.setOnCheckedChangeListener { _, isChecked ->
            // Save theme preference
            sharedPreferences.edit().putBoolean("isDarkTheme", isChecked).apply()
            setAppTheme(isChecked)
        }
    }

    private fun setAppTheme(isDarkTheme: Boolean) {
        if (isDarkTheme) {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
        } else {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
        }
    }
}

This code does the following:

  • Initializes the UI elements and SharedPreferences.
  • Loads the previously saved theme state from SharedPreferences.
  • Sets the initial theme based on the loaded state.
  • Attaches a listener to the Switch to detect theme changes.
  • Saves the new theme state to SharedPreferences whenever the Switch is toggled.
  • Applies the selected theme by calling AppCompatDelegate.setDefaultNightMode().

Step 3: Declare Themes in the AndroidManifest.xml File

Ensure that your application theme is set to a default theme (e.g., LightTheme) in the AndroidManifest.xml file. This will be the initial theme of your application.

AndroidManifest.xml
<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/LightTheme">
    <activity android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

Step 4: Persist Theme Changes

To ensure that the selected theme persists across app sessions, you can save the theme preference using SharedPreferences.

import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.Switch
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate

class MainActivity : AppCompatActivity() {

    private lateinit var themeSwitch: Switch
    private lateinit var sharedPreferences: SharedPreferences

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

        themeSwitch = findViewById(R.id.themeSwitch)
        sharedPreferences = getSharedPreferences("themePref", Context.MODE_PRIVATE)

        // Load saved theme
        val isDarkTheme = sharedPreferences.getBoolean("isDarkTheme", false)
        themeSwitch.isChecked = isDarkTheme
        setAppTheme(isDarkTheme)

        themeSwitch.setOnCheckedChangeListener { _, isChecked ->
            // Save theme preference
            sharedPreferences.edit().putBoolean("isDarkTheme", isChecked).apply()
            setAppTheme(isChecked)
        }
    }

    private fun setAppTheme(isDarkTheme: Boolean) {
        if (isDarkTheme) {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
        } else {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
        }
    }
}

Step 5: Apply the Theme at Runtime

To apply the selected theme, you need to recreate the activity or apply the theme properties dynamically. Using AppCompatDelegate.setDefaultNightMode() automatically applies theme changes, making it straightforward.

Additional Tips for Enhancing Dynamic Themes

  • Custom Attributes: Use custom theme attributes to define app-specific styling, allowing you to tailor the themes to your app’s unique design.
  • Theme Variants: Create variations of themes (e.g., a “Blue Light Theme” and a “Green Light Theme”) to offer more customization options.
  • Testing: Thoroughly test your themes on different devices and Android versions to ensure consistency and compatibility.

Conclusion

Implementing dynamic themes using XML and handling user input can significantly enhance the user experience of your Android application. By following these steps, you can create a customizable, accessible, and engaging app that caters to individual user preferences without requiring complex code changes. Creating dynamic themes with user input in XML is an essential skill for any Android developer looking to provide a more personalized app experience.