Dynamic theming is a powerful technique for Android apps that need flexible branding. In scenarios like white-label products, enterprise clients, or apps that fetch custom settings from a server, being able to update colors at runtime can save you from maintaining multiple static themes or shipping new builds.

In this article, we will explore two practical ways to apply server-defined color schemes in XML-based Android UIs:

What Are Dynamic Color Schemes?

A dynamic color scheme lets your app load and apply a palette at runtime, based on user preferences, company branding, or remote configuration. Instead of hardcoding styles or toggling between predefined themes, the app adapts its appearance dynamically, keeping the UI consistent with the source of truth on the server.

Example server response:

{
  "primary": "#006EAD",
  "secondary": "#00C853",
  "background": "#FFFFFF",
  "surface": "#F5F5F5",
  "onPrimary": "#FFFFFF"
}

(A real-world payload would likely include more fields.)

Setup

We’ll define a simple model to represent our theme colors (omitting DTOs and converters for brevity):

data class ThemeColors(
    val primary: Int,
    val secondary: Int,
    val background: Int,
    val surface: Int,
    val onPrimary: Int
)

Approach 1: Manual View Theming

How It Works

After inflating a layout, you manually apply colors to each view using findViewById, setBackgroundColor, setTextColor, etc.

Example:

class MainActivity : AppCompatActivity() {

    private val themeColors = ThemeColorsRepository.get( /* from server */ )

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

        val root = findViewById<ViewGroup>(R.id.rootLayout)
        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        val titleText = findViewById<TextView>(R.id.titleText)

        toolbar.setBackgroundColor(themeColors.primary)
        toolbar.setTitleTextColor(themeColors.onPrimary)
        root.setBackgroundColor(themeColors.background)
        titleText.setTextColor(themeColors.primary)
    }
}

✅ Pros

❌ Cons

Approach 2: Using LayoutInflater.Factory2

What Is It?

LayoutInflater.Factory2 is a lesser-known but powerful Android API. It lets you intercept view inflation globally and apply logic (like theming) as views are created.

How It Works

Instead of styling views manually, you “wrap” the inflation process and automatically apply colors to views as they’re inflated from XML.

Example

class ThemingFactory(
    private val baseFactory: LayoutInflater.Factory2?,
    private val themeColors: ThemeColors
) : LayoutInflater.Factory2 {

    override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
        val view = baseFactory?.onCreateView(parent, name, context, attrs)
            ?: LayoutInflater.from(context).createView(parent, name, null, attrs)

        applyDynamicTheme(view)
        return view

    }

    override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
        return onCreateView(null, name, context, attrs)
    }

    private fun applyDynamicTheme(view: View?) {
        when (view) {
            is TextView -> view.setTextColor(themeColors.primary)
            is Button -> {
                view.setBackgroundColor(themeColors.primary)
                view.setTextColor(themeColors.onPrimary)
            }
            is Toolbar -> {
                view.setBackgroundColor(themeColors.primary)
                view.setTitleTextColor(themeColors.onPrimary)
            }
        }
    }
}

Installation

This must be set before setContentView:

override fun onCreate(savedInstanceState: Bundle?) {
    val themeColors = ThemeColors(/* from server */)

    val inflater = LayoutInflater.from(this)
    val baseFactory = inflater.factory2
    LayoutInflaterCompat.setFactory2(inflater, ThemingFactory(baseFactory, themeColors))

    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

⚠️ Gotcha: With AppCompatActivity, the inflater is overridden internally. If you don’t delegate back to the default AppCompat factory, you’ll lose default styling. A working sample is available here:

Manual vs Factory2: Feature Comparison

Feature

Manual View Theming

LayoutInflater.Factory2 Theming

Ease of implementation

✅ Beginner-friendly

⚠️ Intermediate

Control per view

✅ Total

⚠️ Needs conditionals per view type

Scalability

❌ Poor (per view)

✅ Excellent (global, centralized)

Boilerplate

❌ High

✅ Low

Reusability

❌ Limited

✅ Easy to reuse across screens

Custom view theming

❌ Manual only

✅ Interceptable during inflation

Dynamic theme switching

⚠️ Manual re-theming required

⚠️ Needs re-inflation or restart

In practice: I applied theming to a large app with dozens of screens in four weeks using LayoutInflater.Factory2. A manual approach would have taken far longer.

Bonus Section: Compose

Jetpack Compose makes it natural to create and apply a custom MaterialTheme dynamically, so you can swap colors at runtime (for example, after fetching them from your server).

Example of implementation:

  1. Define a ThemeColors model (just like in the XML-based version).
  2. Expose it from a ViewModel using StateFlow or LiveData.
  3. Wrap your UI with a MaterialTheme whose colorScheme is derived from ThemeColors.
  4. All Composables that use MaterialTheme.colorScheme will automatically recompose when colors change.

XML + Factory2

Jetpack Compose

Manual theming of views (per type)

Global theming via MaterialTheme

Requires inflating and intercepting views

Native support with recomposition

Boilerplate-heavy

Minimal, declarative

Great for legacy codebases

Best for Compose-first apps

In short, Compose makes dynamic theming a first-class feature, while XML requires custom plumbing (via LayoutInflater.Factory2 or manual updates).

Sample project: Dynamic Theme in Compose

Conclusion

All of the mentioned approaches unlock server-driven dynamic theming, but each fits different needs:

Further Reading