Customizing Material Theme with Jetpack Compose
When Jetpack Compose went stable in July 2021, many Android developers started experimenting with it to see if it’s mature enough to be used in production. Spoiler alert: it’s more than ready and more delightful than XML!
At the same time, we decided to redesign our mobile app called Codeforces WatchR, which is available on both iOS and Android. So it was a no-brainer to execute this redesign for the Android app using Jetpack Compose.
This article will describe the first step in our Jetpack Compose journey — defining and using the Theme. Jetpack Compose theme, to be exact, since, as it’s turned out, XML themes aren’t fully compatible with Jetpack Compose.
@Composable Theme
As you already know, Jetpack Compose is built on the concept of Composable functions. It means that our whole UI is declared as a hierarchy of functions capable of rendering all possible views for all possible states.
The theme doesn’t make an exception to this rule. If we think about it, colors and fonts can easily change while the application is on the screen. Hence we need to recompose our UI to reflect the change (light/dark mode change, for example). So it makes total sense to have a theme as a Composable function.
Even though there is a library that provides partial compatibility between XML and Jetpack Compose themes, the conversion is far from perfect. So we decided to support two sets of themes for the transition period.
The AppCompat Compose Theme Adapter library allows you to easily re-use AppCompat XML themes for theming in Jetpack Compose. It creates a
MaterialTheme
with the color and typography values from the context's theme.
Theme Structure
As discussed above, the theme is a Composable function. It takes the content to be themed and returns … nothing. But it provides an adequately configured theme object to the content by using CompositionLocalProvider
.
In the Theme object, we get a number of systems. There are color, typography, and shape systems in the basic version. But we can add custom systems to the Theme object if needed. All those systems are simple data classes, by the way.
Then we can access those systems and their values in our Composable components as simply as Theme.ColorSystem.color
. If you think it resembles singleton, you are right!
Theme Customization
As soon as we need to add any Composable to our UI, it’s easy to see that they don’t come bundled with Jetpack Compose. Instead, we need to import androidx.compose.material
library.
It gives us a hint about how those components are styled. MaterialTheme
is the correct answer! There is no magic about how this default theme is working. It contains three systems — colors, typography, and shapes.
Material components use values from those systems as default parameters. So to customize the look and feel of our UI, we need to override those values. You can find our first attempt below.
And here, you can find the package with a totality of classes we needed to customize the default material theme to our needs.
Customization Limits
Material systems are final classes, so we can’t inherit them. Nonetheless, there are multiple options if we need more flexibility from the theme:
- Extending
MaterialTheme
with additional theming values - Replacing one or more Material systems —
Colors
,Typography
, orShapes
— with custom implementations, while maintaining the others - Implementing a fully-custom design system to replace
MaterialTheme
Depending on the customization needs, we may choose different options.
Conclusions
So far, we’ve customized the default MaterialTheme
. But taking into account the complexity of our new design and its remoteness from the classical material guidelines, we consider implementing a fully-custom design system.
This will require the complete customization of all material components because the default values won’t be appropriately provided anymore. But the flexibility given by this option may be worth the investment.