Using dp as a Text Unit in Jetpack Compose

In Jetpack Compose, we introduce two ways to set dp as text unit.

Quokkaman
6 min read6 days ago

When attempting to use dp as a text unit in Jetpack Compose, a compile error occurs as shown below

Text(
text = "Unabled to use dp on Text",
fontSize = 10.dp
)

Why we can’t use dp as a text unit?

The fontSize parameter of Text composable function is of type TextUnit.

@Composable
fun Text(
// ...
fontSize: TextUnit = TextUnit.Unspecified,
// ...
) {

sp returns a TextUnit type, whereas dp return Dp, which is not a subtype of TextUnit.

@Stable
val Int.sp: TextUnit get() = pack(UNIT_TYPE_SP, this.toFloat())
@Stable
inline val Int.dp: Dp get() = Dp(value = this.toFloat())

So, to use dp as a text unit, we need to do something.

I suggest two ways to handle this in this posts.

  1. Convert Dp to TextUnit

We would need to convert it to a TextUnit, and it’s very simple if you understand the difference between dp and sp.

If you think about it simply, you can just divide the dp value by the font scale to convert it to a TextUnit.

fun Dp.toSp(density: Density): TextUnit = (this.value / density.fontScale).sp

But there are some issues because Android SDK 34, they started using a non-linear font scale. This could lead to potential side effects when converting dp to TextUnit.

So, We need to convert using the FontScaleConverter, which was introduced to handle the non-linear font scale.

But we don’t need to implement ourselves, because functions like this are already supported.

In FontScaling.android.kt , there is a Dp::toSp function that helps with the conversion.

The things we thought about and considered have already been taken into account and implemented in this function.

We can just use it!


actual interface FontScaling {
/**
* Current user preference for the scaling factor for fonts.
*/
@Stable
actual val fontScale: Float

/**
* Convert [Dp] to Sp. Sp is used for font size, etc.
*/
@Stable
actual fun Dp.toSp(): TextUnit {
if (!FontScaleConverterFactory.isNonLinearFontScalingActive(fontScale) ||
DisableNonLinearFontScalingInCompose) {
return (value / fontScale).sp
}

val converter = FontScaleConverterFactory.forScale(fontScale)
return (converter?.convertDpToSp(value) ?: (value / fontScale)).sp
}

So, we can convert dp to sp through FontScaling.

fun Dp.toSp(fontScaling: FontScaling): TextUnit = with(fontScaling) {
this@toSp.toSp()
}

How we can get fontScaling instance?

Density is subtype of FontScaling. So we can use Density for converting dp to sp.

interface Density : FontScaling

So, we can convert it like this.

fun Dp.toSp(fontScaling: FontScaling): TextUnit = with(fontScaling) {
this@toSp.toSp()
}
Text(
text = "Convert dp to sp through Density",
fontSize = 10.dp.toSp(LocalDensity.current)
)

Normally, we can find the following way because it is more intuitive.

Text(
text = "Convert dp to sp through Density",
fontSize = with(LocalDensity.current) { 10.dp.toSp() }
)

Now we know how to toSp works for converting.

The first method, using transformations, is very common and examples can be easily found on the Internet. But i want to suggest a new way to use dp as a text unit.

We need to think about why we need to set dp instead of sp as a text unit. It is to prevent the size of the component from increasing by not being affected by the text size set by the user setting. Then, we can think of an approach of changing fontScale instead of changing dp to sp.

2. Custom Scope for overriding font scale

TextUnit will be calculated in pixels for drawing. we can find a relevant function in Density.kt.

@Stable
fun TextUnit.toPx(): Float {
checkPrecondition(type == TextUnitType.Sp) { "Only Sp can convert to Px" }
return toDp().toPx()
}

It simply delegates two funcitons TextUnit::toDtp and Dp::toPx.

In FontScaling.android.kt, they are using fontScale for calculating.

/** Current user preference for the scaling factor for fonts. */
@Stable actual val fontScale: Float

// ...

@Stable
actual fun TextUnit.toDp(): Dp {
checkPrecondition(type == TextUnitType.Sp) { "Only Sp can convert to Px" }
if (!FontScaleConverterFactory.isNonLinearFontScalingActive(fontScale)) {
return Dp(value * fontScale)
}

val converter = FontScaleConverterFactory.forScale(fontScale)
return if (converter == null) Dp(value * fontScale) else Dp(converter.convertSpToDp(value))
}

Since FontScaling is an interface, let’s move on to the implementation class Desity.

They use the configuration’s font scale in AndroidDensity.android.kt to adjust the scaling appropriately based on the system’s font scaling settings.

fun Density(context: Context): Density {
val fontScale = context.resources.configuration.fontScale
return DensityWithConverter(
context.resources.displayMetrics.density,
fontScale,
FontScaleConverterFactory.forScale(fontScale) ?: LinearFontScaleConverter(fontScale)
)
}

If we can override the configuration within a specific scope, we can apply a fixed font scale! Then, our sp behave like dp because the fontScale is set a fixed value 1.0.

Compose provides several providers for configuration, such as LocalContext, LocalDensity, and LocalConfiguration. These providers give access to the current context, screen density, and device configuration, respectively, allowing you to customize behavior based on the environment.

I made a sample code to test a new concept that restricts a fixed font scale within a specific scope.

@Composable
fun FixedFontScale(
fontScale: Float = 1.0f,
content: @Composable () -> Unit
) { /* TODO Something */ }

@Composable
fun Content() {
Column {
Text(
text = "Fontscale: 1.0",
fontSize = 10.dp.toSp(LocalDensity.current)
)
Text(
text = "Fontscale: ${LocalDensity.current.fontScale}",
fontSize = 10.sp
)
FixedFontScale {
Text(
text = "Font scale should be 1.0",
fontSize = 10.sp
)
}

FixedFontScale {
Text(
text = "Font scale should be 1.0",
fontSize = dimensionResource(R.dimen.sp10).toSp(LocalDensity.current)
)
}
}
}

And i set the font scale using an ADB command.

adb shell set settings put system font_scale 2.0

Now, let’s implement the FixedFontScale function.

First i tried to use LocalConfiguration for fixed font scale.

@Composable
fun FixedFontScale(
fontScale: Float = 1.0f,
content: @Composable () -> Unit
) {

CompositionLocalProvider(
LocalConfiguration provides Configuration().apply {
setTo(LocalConfiguration.current)
this.fontScale = fontScale
},
content = content
)
}

But… It doesn’t work well. It seems the logic for converting sp to pixels isn’t directly using LocalConfiguration.

Secondly, we can try using LocalDensity like this.

@Composable
fun FixedFontScale(
fontScale: Float = 1.0f,
content: @Composable () -> Unit
) {

CompositionLocalProvider(
LocalDensity provides Density(
density = LocalDensity.current.density,
fontScale = fontScale,
),
content = content
)
}

It works well when using sp directly, but when using dimensionResource, it doesn’t works as expected.

Thirdly, we can try updating LocalContext.

@Composable
fun FixedFontScale(
fontScale: Float = 1.0f,
content: @Composable () -> Unit
) {

CompositionLocalProvider(
LocalContext provides LocalContext.current.createConfigurationContext(
Configuration().apply {
this.fontScale = fontScale
}
),
content = content
)
}

Oops, this time, it only works with the XML case.

They are using different implementation in two ways: one with TextUnit and the other with dimensionResources.

We need to consider both approaches.

If your app don’t use dimens.xml, you don’t need to override context :)

Below is the final version of FixedFontScale.

@Composable
fun FixedFontScale(
fontScale: Float = 1.0f,
content: @Composable () -> Unit
) {

CompositionLocalProvider(
LocalContext provides LocalContext.current.createConfigurationContext(
Configuration().apply {
this.fontScale = fontScale
}
),
LocalDensity provides Density(
density = LocalDensity.current.density,
fontScale = fontScale,
),
content = content
)
}

It works perfectly! Now we can create a specific scope that allows for a fixed font scale.

Summary

There are several ways to use dp as a TextUnit in Compose. I’ll introduce two approaches: one is converting with FontScaling, and the other is creating a scope with a fixed font scale.

The second approach can be expanded to allow for a specific range of font scales :)

--

--

No responses yet