DEV Community

Aleksei Laptev
Aleksei Laptev

Posted on • Originally published at mardsoul.github.io

Implementing Chaos-Proof Custom Typography in Compose

Problem case

Recently I met a case when the typography system in design was so terrible that I spent lots of time with tries to make it into some more beautiful code. What was in that case? - Many different font weghts and sizes.

You may ask: "And so what?" - There were simply too many, such as on one screen a title can be with size 60px and font weight SemiBold, on another - size 54px and font weght Medium. And lots of them... The font list was impossble to be grouped into the Material Design typography system (you know, DisplayLarge, DisplayMedium and so on).

Earlier I wanna named this article like a "How to be if your designer is an ass-hole" but it have been incorrectly because in generally I meet professionals. It was an exception case.


Local solution

On-site definition

Let's define text style in place.

@Composable
fun SomeScreen() {

    val textStyle = remember {
        TextStyle(
            fontFamily = <family>,
            fontWeight = <weght>,
            fontSize = <size>,
        )
    }

    Text(
        text = "some text",
        style = textStyle
    )

// etc

}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • easy
  • visible
  • easy to maintain if the local changes is needs

Cons:

  • unusable if you need apply in another functions
  • remember block or composable resources (can't use composable functions in the remember block)
  • hard to maintain if the global changes is needs

Summary:
For my case it's the worst approach. But in another cases like, you have a clean design with consistent typography and in one place you have to set some exclusive font, it make sense.

In the module defenition

Like the previous approach you define text styles but at another visible level - in the module.

internal object ModuleTypography {
    val titleStyle = TextStyle(
        fontFamily = <family>,
        fontWeight = <weight>,
        fontSize = <size>,
    )

    val contentStyle = TextStyle(
        fontFamily = <family>,
        fontWeight = <weight>,
        fontSize = <size>,
    )

//etc

}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • easy
  • visible
  • available in module

Cons:

  • unusable if you need apply in another module
  • duplicate styles in different modules

Summary:
It's quite a neat approach. And in case you have different fonts mostly strongly by features it make sense.

Custom Typography system

Create a system

So, okay, the traditional system is insufficient for us. It means that we have to make our own typography.

data class AppTypo(
    val topBar: TextStyle,
    val screen1: Screen1Typo,
    val screen2: Screen2Typo,
// etc
)

data class Screen1Typo(
    val tableTitle: TextStyle,
    val tableContent: TextStyle,
    val common: TextStyle,

// etc

)

// etc
Enter fullscreen mode Exit fullscreen mode

Now we have to define values. We can can set them to a class constructor as a default or make a singletone variable.

internal val CustomTypo = AppTypo(
    topBar = TextStyle(
        fontFamily = <family>,
        fontWeight = <weight>,
        fontSize = <size>,
    ),
    screen1 = Screen1Typo(
        tableTitle = TextStyle(
            fontFamily = <family>,
            fontWeight = <weight>,
            fontSize = <size>,
        ),
        tableContent = TextStyle(
            fontFamily = <family>,
            fontWeight = <weight>,
            fontSize = <size>,
        ),

// etc

)
Enter fullscreen mode Exit fullscreen mode

Integrate to Compose

For this action we will use CompositionLocal, in simple words it's a dependency injection for Compose UI system.

  • define the CompositionLocal
internal val LocalAppTypography = staticCompositionLocalOf { CustomTypo } //static for avoiding tracking
Enter fullscreen mode Exit fullscreen mode
  • provide into the whole theme
@Composable
fun MaterialStaticTheme(
    isDark: Boolean = true,
    content: @Composable () -> Unit,
) {

//some code

    CompositionLocalProvider(
        LocalAppTypography provides CustomTypo
    ) {
        MaterialTheme() {
            content()
        }
    }

//some code

}
Enter fullscreen mode Exit fullscreen mode
  • add direct access to an object
object AppTheme {
    val typo: AppTypo
        @Composable
        @ReadOnlyComposable
        get() = LocalAppTypography.current
}
Enter fullscreen mode Exit fullscreen mode
  • use in any place of UI
@Composable
fun SomeScreen() {

//some code

    Text(
        text = "some text",
        style = AppTheme.typo.screen1.tableContent
    )

//some code

}
Enter fullscreen mode Exit fullscreen mode

Additional

It make sense to create a single point of enter to application theme.

object AppTheme {
    val typo: AppTypo
        @Composable
        @ReadOnlyComposable
        get() = LocalAppTypography.current

    val colorScheme: ColorScheme
        @Composable
        @ReadOnlyComposable
        get() = MaterialTheme.colorScheme

    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = MaterialTheme.shapes
}
Enter fullscreen mode Exit fullscreen mode

In that case you can use AppTheme instead of MaterialTheme object also for colors and shapes.

Pros:

  • available in the whole app
  • easy for maintaining

Cons:

  • much boilerplate code

Summary:
So, this solution is for cases like mine. If there are so many styles that it can't be grouped to standard typography.


Thanks for reading. I wish you good designers! :)

Top comments (0)