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: Manual View Theming Using LayoutInflater.Factory2 We will compare the two approaches in terms of scalability, maintainability, and complexity, and also look briefly at how Jetpack Compose makes dynamic theming first-class. Manual View Theming Using LayoutInflater.Factory2 We will compare the two approaches in terms of scalability, maintainability, and complexity, and also look briefly at how Jetpack Compose makes dynamic theming first-class. 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" } { "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 ) 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. findViewById setBackgroundColor setTextColor 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) } } 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 Beginner-friendly and easy to debug. Great for prototypes or theming a few views. Beginner-friendly and easy to debug. Great for prototypes or theming a few views. ❌ Cons Tedious in multi-screen apps. Easy to miss views and lose consistency. Doesn’t scale well. Tedious in multi-screen apps. Easy to miss views and lose consistency. Doesn’t scale well. 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. LayoutInflater.Factory2 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) } } } } 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: 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) } 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: HomeActivity.kt ThemingFactory.kt HomeActivity.kt HomeActivity.kt ThemingFactory.kt ThemingFactory.kt 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 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 Feature Manual View Theming LayoutInflater.Factory2 Theming Feature Feature Manual View Theming Manual View Theming LayoutInflater.Factory2 Theming LayoutInflater.Factory2 Theming Ease of implementation ✅ Beginner-friendly ⚠️ Intermediate Ease of implementation Ease of implementation ✅ Beginner-friendly ✅ Beginner-friendly ⚠️ Intermediate ⚠️ Intermediate Control per view ✅ Total ⚠️ Needs conditionals per view type Control per view Control per view ✅ Total ✅ Total ⚠️ Needs conditionals per view type ⚠️ Needs conditionals per view type Scalability ❌ Poor (per view) ✅ Excellent (global, centralized) Scalability Scalability ❌ Poor (per view) ❌ Poor (per view) ✅ Excellent (global, centralized) ✅ Excellent (global, centralized) Boilerplate ❌ High ✅ Low Boilerplate Boilerplate ❌ High ❌ High ✅ Low ✅ Low Reusability ❌ Limited ✅ Easy to reuse across screens Reusability Reusability ❌ Limited ❌ Limited ✅ Easy to reuse across screens ✅ Easy to reuse across screens Custom view theming ❌ Manual only ✅ Interceptable during inflation Custom view theming Custom view theming ❌ Manual only ❌ Manual only ✅ Interceptable during inflation ✅ Interceptable during inflation Dynamic theme switching ⚠️ Manual re-theming required ⚠️ Needs re-inflation or restart Dynamic theme switching Dynamic theme switching ⚠️ Manual re-theming required ⚠️ Manual re-theming required ⚠️ Needs re-inflation or restart ⚠️ 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. LayoutInflater.Factory2 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: Define a ThemeColors model (just like in the XML-based version). Expose it from a ViewModel using StateFlow or LiveData. Wrap your UI with a MaterialTheme whose colorScheme is derived from ThemeColors. All Composables that use MaterialTheme.colorScheme will automatically recompose when colors change. Define a ThemeColors model (just like in the XML-based version). ThemeColors Expose it from a ViewModel using StateFlow or LiveData. Wrap your UI with a MaterialTheme whose colorScheme is derived from ThemeColors. All Composables that use MaterialTheme.colorScheme will automatically recompose when colors change. MaterialTheme.colorScheme 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 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 XML + Factory2 Jetpack Compose XML + Factory2 XML + Factory2 Factory2 Jetpack Compose Jetpack Compose Manual theming of views (per type) Global theming via MaterialTheme Manual theming of views (per type) Manual theming of views (per type) Global theming via MaterialTheme Global theming via MaterialTheme MaterialTheme Requires inflating and intercepting views Native support with recomposition Requires inflating and intercepting views Requires inflating and intercepting views Native support with recomposition Native support with recomposition Boilerplate-heavy Minimal, declarative Boilerplate-heavy Boilerplate-heavy Minimal, declarative Minimal, declarative Great for legacy codebases Best for Compose-first apps Great for legacy codebases Great for legacy codebases Best for Compose-first apps 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). LayoutInflater.Factory2 Sample project: Dynamic Theme in Compose Dynamic Theme in Compose Conclusion All of the mentioned approaches unlock server-driven dynamic theming, but each fits different needs: Manual theming: Best for small apps, quick prototypes, or theming just a few views. LayoutInflater.Factory2: The way to go for scalable, brand-flexible apps (white-label, multi-client). Jetpack Compose: Dynamic theming is built-in and declarative, ideal for new projects. If you’re working on a legacy XML app, Factory2 will save you huge amounts of time. For new apps, Compose + MaterialTheme is the clear winner. Manual theming: Best for small apps, quick prototypes, or theming just a few views. LayoutInflater.Factory2: The way to go for scalable, brand-flexible apps (white-label, multi-client). Jetpack Compose: Dynamic theming is built-in and declarative, ideal for new projects. If you’re working on a legacy XML app, Factory2 will save you huge amounts of time. For new apps, Compose + MaterialTheme is the clear winner. Further Reading Android Docs: LayoutInflater.Factory2 Sample project Android Docs: LayoutInflater.Factory2 Android Docs: LayoutInflater.Factory2 Sample project Sample project