Kotlin XML: Theming Android Apps with Theme Attributes (?attr/, ?android:attr/)

When developing Android applications using Kotlin and XML, creating a consistent and customizable user interface is crucial. Android’s theming system, combined with theme attributes (?attr/ and ?android:attr/), provides a powerful mechanism for achieving this goal. This blog post will explore how to leverage theme attributes to build dynamic and maintainable themes in your Android applications.

What are Theme Attributes?

Theme attributes are placeholders defined in your application’s theme that refer to specific styling properties such as colors, dimensions, or even complex resources like drawables. They act as variables within your XML layouts, allowing you to reference and modify the visual aspects of your UI components easily through theme changes. There are two main types of theme attributes:

  • Custom Attributes (?attr/): These are attributes that you define in your attrs.xml file, allowing you to create custom theming options tailored to your application’s design system.
  • Android Attributes (?android:attr/): These are pre-defined attributes provided by the Android framework that cover a wide range of common styling properties.

Why Use Theme Attributes?

  • Centralized Styling: Manage all style-related properties in a single place (the theme).
  • Dynamic Theming: Easily switch between different themes (light, dark, custom) by changing theme attribute values.
  • Consistency: Ensure UI components across your application maintain a consistent look and feel.
  • Maintainability: Updates to the theme automatically propagate to all components referencing the attributes.

How to Use Theme Attributes in Kotlin XML Development

Let’s explore how to define and use both custom and Android theme attributes in an Android project using Kotlin and XML.

Step 1: Define Custom Attributes in attrs.xml

Create a file named attrs.xml in your res/values/ directory (or modify it if it already exists) and define your custom attributes.


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomThemeAttributes">
        <attr name="customBackgroundColor" format="color"/>
        <attr name="customTextColor" format="color"/>
        <attr name="customButtonRadius" format="dimension"/>
    </declare-styleable>
</resources>

In this example, we’ve declared a styleable named CustomThemeAttributes with three custom attributes: customBackgroundColor, customTextColor, and customButtonRadius.

Step 2: Define Theme Styles in styles.xml

Open your res/values/styles.xml file and define your themes, setting the values for your custom attributes.


<resources>
    <style name="BaseTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
        <!-- Customize your theme here. -->
    </style>

    <style name="LightTheme" parent="BaseTheme">
        <item name="customBackgroundColor">#FFFFFF</item>
        <item name="customTextColor">#000000</item>
        <item name="customButtonRadius">8dp</item>
    </style>

    <style name="DarkTheme" parent="BaseTheme">
        <item name="customBackgroundColor">#303030</item>
        <item name="customTextColor">#FFFFFF</item>
        <item name="customButtonRadius">12dp</item>
    </style>
</resources>

Here, we’ve defined two themes: LightTheme and DarkTheme, both inheriting from a base theme BaseTheme. We set different values for the custom attributes in each theme to achieve a distinct look and feel.

Step 3: Apply the Theme in AndroidManifest.xml

Set the application’s theme in your AndroidManifest.xml file.


<application
    android:name=".App"
    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"> <!-- Set default theme here -->
    <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>

By default, the LightTheme is applied to the entire application. You can programmatically change the theme at runtime (covered later).

Step 4: Use Theme Attributes in Layout XML

In your layout XML files, reference the theme attributes using the ?attr/ syntax.


<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="?attr/customBackgroundColor">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello, Theme!"
        android:textColor="?attr/customTextColor"/>

    <Button
        android:id="@+id/myButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click Me"
        android:backgroundTint="?attr/customBackgroundColor"
        android:textColor="?attr/customTextColor" />
</LinearLayout>

The android:background of the LinearLayout and android:textColor of the TextView and Button are dynamically determined by the customBackgroundColor and customTextColor attributes defined in the applied theme.

Step 5: Using Android Attributes (?android:attr/)

Android provides a rich set of pre-defined attributes you can use in a similar way. For example, to reference the colorPrimary attribute from your theme, you would use ?android:attr/colorPrimary.


<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?android:attr/colorPrimary"
    android:theme="?attr/actionBarTheme" />

Step 6: Programmatically Changing Themes in Kotlin

You can change the application theme programmatically at runtime using the setTheme() method.


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

class MainActivity : AppCompatActivity() {

    private lateinit var sharedPreferences: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        sharedPreferences = getSharedPreferences("ThemePrefs", Context.MODE_PRIVATE)
        val isDarkMode = sharedPreferences.getBoolean("isDarkMode", false)

        if (isDarkMode) {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            setTheme(R.style.DarkTheme)
        } else {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
            setTheme(R.style.LightTheme)
        }

        setContentView(R.layout.activity_main)
    }

    // Method to toggle theme
    fun toggleTheme() {
        val editor = sharedPreferences.edit()
        val isDarkMode = sharedPreferences.getBoolean("isDarkMode", false)

        if (isDarkMode) {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
            setTheme(R.style.LightTheme)
            editor.putBoolean("isDarkMode", false)
        } else {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            setTheme(R.style.DarkTheme)
            editor.putBoolean("isDarkMode", true)
        }

        editor.apply()

        // Recreate activity to apply changes (consider alternatives for smoother transition)
        recreate()
    }
}

Explanation:

  • Theme Preference: Stores the user’s theme preference in SharedPreferences to persist across app restarts.
  • Applying Theme: Sets the appropriate theme using setTheme() before calling setContentView(). This ensures the correct theme is applied before the UI is drawn.
  • Toggling Theme: The toggleTheme() function toggles between the light and dark themes, saves the preference, and recreates the activity. Recreating the activity applies the theme changes, but consider alternative approaches for smoother transitions.

Best Practices for Using Theme Attributes

  • Consistency is Key: Ensure your custom attributes are named consistently throughout your application.
  • Comment Your Attributes: Add comments to your attrs.xml file explaining the purpose of each attribute.
  • Use Android Attributes When Appropriate: Leverage Android’s pre-defined attributes when they meet your styling needs.
  • Create Base Themes: Define a base theme and extend it for different variations (light, dark, etc.).
  • Test Thoroughly: Test your themes on different devices and screen sizes to ensure a consistent experience.

Conclusion

Using theme attributes (?attr/ and ?android:attr/) is a powerful technique for creating themable and maintainable Android applications using Kotlin and XML. By leveraging custom attributes, predefined Android attributes, and proper theming practices, you can build apps that provide a consistent, customizable, and delightful user experience. Understanding and applying these concepts will greatly improve the architecture and quality of your Android projects.