TL;DR
Applying a theme to a drawable within compose can be a bit tricky as
Xml drawables can not access color definitions in Jetpack Compose. Therefore you should define your colors and themes in Xml and make Jetpack Compose aware of those values.
Use AndroidView within Compose for loading an xml drawable:
AndroidView(factory = { context ->
val contextThemeWrapper = ContextThemeWrapper(context, R.style.RedTheme)
val drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.your_drawable, contextThemeWrapper.theme)
ImageView(context).apply { setImageDrawable(drawable) }
})
This way the drawable will respect the theme style you are passing to ContextThemeWrapper.
Define a Theme in Xml
Along with your defined themes RedTheme and GreenTheme you also need to declare those attributes:
You also need to define the style attributes:
<declare-styleable name="ThemeStyle">
<attr name="colorPrimary" format="color" />
<attr name="colorSecondary" format="color" />
</declare-styleable>
Map Theme to Compose
Define an AppTheme class:
object AppTheme {
val colors: AppColors
@Composable
@ReadOnlyComposable
get() = LocalColors.current
val style: Int
@Composable
@ReadOnlyComposable
get() = LocalStyleRes.current
}
internal val LocalStyleRes: ProvidableCompositionLocal<Int> = staticCompositionLocalOf { R.style.RedTheme }
@Composable
fun AppTheme(
@StyleRes styleRes: Int,
context: Context,
content: @Composable () -> Unit,
) {
val themeReader = ThemeReader(context, styleRes)
val colors = AppColors(
primary = Color(themeReader.colorPrimary),
secondary = Color(themeReader.colorSecondary),
)
CompositionLocalProvider(
LocalColors provides colors,
LocalStyleRes provides styleRes
) {
content()
}
}
And a AppColor class:
data class AppColors(
val primary: Color,
val secondary: Color,
)
internal val LocalColors: ProvidableCompositionLocal<AppColors> = staticCompositionLocalOf {
AppColors(
primary = Color.White,
secondary = Color.White
)
}
Read Theme from Xml
Create a ThemeReader class that can read attributes for a style theme.
class ThemeReader(
context: Context,
@StyleRes styleRes: Int
) {
private val attributes: IntArray = intArrayOf(R.attr.primary, R.attr.success, R.attr.error)
private val typedArray: TypedArray = context.obtainStyledAttributes(styleRes, attributes)
val colorPrimary: Int = typedArray.getColor(R.styleable.ThemeStyle_colorPrimary, -1)
val colorSecondary: Int = typedArray.getColor(R.styleable.ThemeStyle_colorSecondary, -1)
}
Loading the Xml Drawable
To ensure that your loaded xml drawable is aware of your theme colors you should not load your drawable with Compose like:
Image(
painter = painterResource(iconTypeResId),
contentDescription = null
)
rather than loading it the classic way as an ImageView and wrapping it into Compose AndroidView. Lets create a composable for this:
@Composable
fun XmlDrawable(
@DrawableRes drawableResId: Int,
) {
@StyleRes val styleRes: Int = AppTheme.style
AndroidView(factory = { context ->
val contextThemeWrapper = ContextThemeWrapper(context, styleRes)
val drawable = ResourcesCompat.getDrawable(context.resources, drawableResId, contextThemeWrapper.theme)
ImageView(context).apply { setImageDrawable(drawable) }
})
}
Now we can apply what we have done.
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// instantiate our custom theme
AppTheme(
styleRes = R.style.RedTheme, // set the theme you wanna use
context = requireContext()
) {
XmlDrawable(drawableResId = R.drawable.your_drawable) // load the xml drawable
// you can also access any AppColor within the composition tree like: AppTheme.colors.primary
}
}
}
}
Reference