Blog Infos
Author
Published
Topics
,
Published

In my opinion, working with theming and designs isn’t that easy. It would be if every app was designed 100% based on Material Design and Android in mind. Keep dreaming 😉

The purpose of this article is to share my experience with designs and implementation in the View System and how it worked out for me in Jetpack Compose lately.

Google has a dedicated site for Material where you will find tons of information explaining in great detail what material is, from what elements it’s built, and how you can implement individual UI components. However, it’s extremely overwhelming if you are not a design veteran. So, I will try to break it down for you as an Android developer to another Android developer in my own words and I will limit that knowledge to what is really needed.

Let’s Go!

Material Design is a ready-to-use design system built for different platforms including Android. What “system” means is simply a set of sub-systems that we put together to control how our app feels and looks.

There are 3 main sub-systems that you should learn about:

  • Typography — define set of text styles
  • Colors — define set of colors mixed with opacity, ripple effects, text selections
  • Shapes — define set of 3 shape groups: small, medium large

A very good place to look at in documentation is Jetpack Compose Anatomy of a theme but I will try to explain how I wrap that around my head.

Each of those sub-systems we divide into schema and values. I will picture this for you with the following diagram:

Design sub system structure

I have skipped some parts of the schema in colors and typography to keep it simple.

Let’s take a look at Color Schema. It contains colors like Primary or Background. It doesn’t say blue or red because we want those colors to have some meaning and it means where those colors suppose to be used. However, we also don’t want them to be very specific like SendEmailButton or anything that leads to specific functionality in the app. Those colors should be reusable and group together similar UI components.

Another reason why we don’t use specific colors blue or red, that I mentioned above, is because depending on the configuration we might actually get a different value. For example, Primary in light mode can be blue and gray in dark mode.

Material Design Components

It’s important to know that the UI components from the Material library refer to schema properties specified in Material Design. That is why different components look good out of the box. It is also worth knowing which schema properties are used where. As an example, below you can see how Material Design distributes most components into 3 shape sizes.

https://material.io/design/shape/applying-shape-to-ui.html#shape-scheme

 

With Jetpack Compose, I highly recommend browsing the implementation to see what Material uses as default. Here is a Card as an example:

 

@Composable
fun Card(
    modifier: Modifier = Modifier,
    shape: Shape = MaterialTheme.shapes.medium,
    backgroundColor: Color = MaterialTheme.colors.surface,
    contentColor: Color = contentColorFor(backgroundColor),
    border: BorderStroke? = null,
    elevation: Dp = 1.dp,
    content: @Composable () -> Unit
) {
    Surface(
        modifier = modifier,
        shape = shape,
        color = backgroundColor,
        contentColor = contentColor,
        elevation = elevation,
        border = border,
        content = content
    )
}

Card use MaterialTheme.shapes.medium as shape and MaterialTheme.colors.surface as background color.

You might also sometimes hear about the style guide. I am not an expert on this but according to my understanding that is basically a plain set of fonts, colors, dimensions etc. If you would go back to my diagram above that would be just the left boxes: Color Palette, Fonts, Shapes, and nothing else.

Real-life

I could move on straight to tell you how to build your own system on top of Material but I need to address one issue first.

There are cases when it gets complicated to work with a design system:

  • design is created with raw values, style guide might be provided but that basically means there is no design system
  • the design was derived from web design which is common and enforce moving away from built-in Material
  • the design uses a high level of customization for most UI components and all namings look nothing like Material

As long as we have a design system in place that separates the scheme from values we are saved. The level of customization just extends the time needed to code it but that is what we get paid for 🙂

The problem is with the lack of a design system. Especially, if the app you build suppose to last a long time and be actively maintained over time. Because that is when the design system truly shines when more feature is built or branding evolve. Without it, making changes simply hurts like hell. So, what to do in that case?

Strive to build a design system together with your team and stick to it. Even if it’s not perfect with clever names, it’s still better than nothing. A design system can also evolve as long as you have one and you do it gradually.

For me, the design system represents branding I am going to apply to the app. Apart from used fonts, colors, or shapes it also:

  • tell me what level of theme customization my app needs
  • is a contract between developers and designers
  • is a kind of communication platform regarding design issues
  • allow easy design modifications in the future
Tooling

There is no design system without tools. I believe that currently, Figma is a very popular one, or at least the one I have seen in use. It’s a tool where designers create beautiful designs and developers read to apply them in the code. It provides layouts for specific screens in the app and style guides with a design system describing the branding. It can also contain common UI components that designers use across different screens. It is also a good practice to follow that approach in the code building for example custom button.

There are 3 important things in such a tool:

  • it translates designs into platform (Web, iOS, Android) specific properties that we should use, e.g. how much DP should be set between the edge of the screen and elements on the screen; you wouldn’t like to measure pixels from the design provided as PNG and then translate as DP yourself 🙂
  • specific screen designs refer to design system, so when you click in Figma on a Button you will see that it uses a color from Color system scheme like primary
  • enable communication between designers and devs by adding comments – its a really good way to resolve issues or misunderstanding

In the past, I was also using Zeplin that developers used and it was integrated with Sketch that designers were using. Can’t really tell which solution is better. In the end, both solutions depend on how much experience designers have with those tools and how much they are willing to cooperate with developers and vice versa.

That is the last part of the article. Custom design system. There are multiple ways to approach it with Jetpack Compose theming. In fact, Google did a really good job describing that in their docs. This is where I have learned what can be done and I highly recommend that you do the same.

I don’t want to copy what they already explained. If you are reading this you probably searched for some guides and didn’t know it’s already in official docs, or you looked for something that gives you a better overview on possible options, and what are the prerequisites to apply one of them. That is exactly where I want to step in.

LocalComposition

Before you jump into theming customization you HAVE TO understand LocalComposition and how that mechanism works. So put away the theme on the side and dive into that first.

CompositionLocal is a tool for passing data down through the Composition implicitly

👆 That is exactly what it is according to the docs. That is how you can for example get Android Context by simply calling LocalContext.current wherever in @Composable That’s why it is implicit, no need to pass it as a composable parameter. There are many existing local compositions like the one I mentioned. Go to the IDE in any @Composable and start typing Local

Q: What do you need LocalComposition for in theming?

A: It’s used to pass down theme sub-systems for your disposal. That is how MaterialTheme does it and that is how you will do it.

LocalComposition doc is not short but If I would have to highlight what is the most important to know it would be the difference between compositionLocalOfand staticCompositionLocalOf described as follow:

There are two APIs to create a CompositionLocal:

  • compositionLocalOf: Changing the value provided during recomposition invalidates only the content that reads its current value.
  • staticCompositionLocalOf: Unlike compositionLocalOf, reads of a staticCompositionLocalOf are not tracked by Compose. Changing the value causes the entirety of the content lambda where the CompositionLocal is provided to be recomposed, instead of just the places where the current value is read in the Composition.

If you have Flutter experience, this mechanism is basically the same what InheritedWidget is for widgets in Flutter.

Referencing MaterialTheme

It is worth noting how we reference MaterialTheme in the code. Whenever you start typing MaterialTheme in the code, two things popup.

First, it’s @Composable MaterialTheme that you will use somewhere at the top of your hierarchy directly or wrapping in your own @Composable Later one is how Android Studio creates the Jetpack Compose project from the wizard.

Second, it’s Object MaterialTheme that is a class that you will be using across your UI code applying fonts, color, or other sub-systems.

In the next section, I will showcase possible options and want to make it clear that whenever there is an option without Custom Theme it means there is noObject CustomTheme but having just@Composable CustomTheme that only setup Material is always a good option. Hope that is clear 😅

Custom design options

 

Option ordered with the simplest being first

Job Offers

Job Offers


    Kotlin Multiplatform Mobile Developer

    Touchlab
    Remote
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

,

Model-driven navigation with Jetpack Compose – From zero to hero

Do we think of navigation as a UI-related concept, or can it be business logic-first? Where are the boundaries between navigation, application state, and animation?
Watch Video

Model-driven navigation with Jetpack Compose – From zero to hero

Zysolt Kocsi
Staff Android Engineer
Bumble

Model-driven navigation with Jetpack Compose – From zero to hero

Zysolt Kocsi
Staff Android Engine ...
Bumble

Model-driven navigation with Jetpack Compose – From zero to hero

Zysolt Kocsi
Staff Android Engineer
Bumble

Jobs

Option 1 — Extensions

This is the simplest approach. It’s the only one when you don’t actually need LocalComposition.

// somewhere in your theming code
val Colors.mySpecialColor: Color
get() = if (isLight) Color.Black else Color.White
// somewhere in the layouts
Surface(color = MaterialTheme.colors.mySpecialColor) {
}
view raw Option1.kt hosted with ❤ by GitHub

Option 1 with a simple extension

 

That is a good option when you rely almost completely on Material Design and you have just a few extra properties to add.

Option 2 — Additional sub-system as Material extension

This one also relies on extensions but unlike option 1 we will be extending the entire theme with a completely new LocalComposition.

// somewhere in the theming code
data class Dimensions(
val tiny: Dp = 4.dp,
val mediocre: Dp = 16.dp,
val craaaazy: Dp = 100.dp
)
val LocalDimensions = staticCompositionLocalOf { Dimensions() }
val MaterialTheme.dimensions
@Composable
@ReadOnlyComposable
get() = LocalDimensions.current
// somewhere at top level of your UI hierarchy
CompositionLocalProvider(LocalDimensions provides Dimensions()) {
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
// somewhere in the layouts
Surface(modifier = Modifier.padding(MaterialTheme.dimensions.craaaazy)) {
}

Option 2 with sub-system as an extension

 

  • Create data class representing the sub-system you want to expose and properties it has
  • Create LocalDimensions as static composition, as a body of composition creates an instance of the sub-system.
  • Expose LocalDimensions.current as an extension of MaterialTheme That way it will look like yet another Material Design sub-system
  • Wrap MaterialTheme with CompositionLocalProvider that again creates Dimensions instance and provide down the hierarchy. You can provide different instances depending on your configuration.

According to Google’s documentation, we should separate the default creation of LocalComposition with some unspecified/dummy value like Dp.Unspecifiedfrom the real instance created when building the theme but it’s up to you.

Philipp Lackner released a nice video about this option👇 I like his content so hopefully he will get a few more subscribers thanks to this article 🥂

In my opinion, working with theming and designs isn’t that easy. It would be if every app was designed 100% based on Material Design and Android in mind. Keep dreaming ;)The purpose of this article is to share my experience with designs and implementation in the View System and how it worked out for me in Jetpack Compose lately.

Option 3 — Custom theme on top of Material

We are getting deeper into the rabbit hole. This time, apart from the new sub-system from option 3, we will add Object CustomTheme that we can use it along with Object MaterialTheme within the layouts. Also, I will add a completely new sub-system for font styles.

// somwhere in the theming code
data class MyTypography(
val bigText: TextStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
),
val smallText: TextStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
)
val LocalSimpleTypography = staticCompositionLocalOf { MyTypography() }
object CustomTheme {
val dimensions
@Composable
@ReadOnlyComposable
get() = LocalDimensions.current
val typography
@Composable
@ReadOnlyComposable
get() = LocalSimpleTypography.current
}
// somewhere at the top level of your UI hierarchy
CompositionLocalProvider(
LocalDimensions provides Dimensions(),
LocalSimpleTypography provides MyTypography()
) {
MaterialTheme(
colors = colors,
shapes = Shapes,
content = content
)
}
// somewhere in the layouts
Text(
text = "Hello",
style = CustomTheme.typography.bigText
)
  • MyTypography created similar to Dimensions from option 3
  • On purpose skipped setting up Material’s typography
  • Added CustomTheme object and used it on Text component

It can be a bit weird at first why would we need separate theme object but you might want to have a more clear separation from MaterialTheme the more customization you make. As you can see, I have skipped Material’s typography because of that new typography being introduced with bigText and smallTextproperties.

Of course, nothing stops you from making new typography as an extension. It is really up to you how you draw the line between Material and custom sub-systems.

Option 4 — Custom theme without Material

This one is the complete custom theme solution. If you understand previous options and how to build LocalComposition and custom theme object then it’s actually simple. Just remove completely use of @Composable MaterialTheme in your code:

CompositionLocalProvider(
LocalDimensions provides Dimensions(),
LocalSimpleTypography provides MyTypography(),
LocalMyColors provides MyColors()
) {
// MaterialTheme(
// colors = colors,
// shapes = Shapes,
// content = content
// )
content()
}
  • MaterialTheme removed, I just kept it in sample commented be clear from where it’s removed

This is the approach if your design system is completely different from Material and there is basically nothing to re-use from there.

Custom UI components

Google in their docs also mentions reusing material components and that is important. Even though we might not be using the Material theme we might still use UI components from the Material library. As mentioned earlier, those components will still try to use sub-systems from MaterialTheme object. Therefore, instead of calling CustomTheme.typography.bigText everywhere you might also create custom @Composable which uses it:

@Composable
fun BigText(text: String, modifier: Modifier = Modifier) {
Text(
text = text,
modifier = modifier,
style = CustomTheme.typography.bigText
)
}

I will extend this section if I encounter more interesting issues to share or if someone ask interesting question in the comments.

Q: What if my design uses something like Material but there are some properties used on a design that doesn’t match any sub-system and I am not able to group them.

A: I suggest creating an extension for every case and making it very specific. Let’s say you have a color that shows up on many different text components that are not related. Make an extension for each case and put it somewhere in the theme package.

val SuspiciousColor = Color(0xFF0F0F0F)
val Colors.specialCardTitle: Color
get() = SuspiciousColor
val Colors.profileTitle: Color
get() = SuspiciousColor
val Colors.settingFooter: Color
get() = SuspiciousColor

That way you won’t expose SuspiciouseColor but a list of specific properties and in the future, once your app gets far with development and it hopefully gets some reasoning behind that color, you will be able to easily identify where it’s used by looking at this single file and then refactor it.

That is all folks. I have tried my best to explain what is Material Design, design system and how one can be extended or build in Jetpack Compose. It got a bit longer than I anticipated but if you like it leave a 👏 or few. I hope you also liked my drawings, it was a challenge on its own 😅

Credits for reviewing goes to Piotr Rutka

 

Thank you for reading and have a happy coding.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
blog
Yes! You heard it right. We’ll try to understand the complete OTP (one time…
READ MORE

Leave a Reply

Your email address will not be published.

Fill out this field
Fill out this field
Please enter a valid email address.

Menu