Looking at `Dp` class in Jetpack Compose

Hits

Why do @Composable functions in Jetpack Compose usually use Dp.Unspecified as default value? Why not use null? The optimization of Jetpack Compose under the hood

  • Author: Petrus Nguyễn Thái Học
  • Published on Sep 16, 2023
  • 5 minutes read so far
  • Tags: #hoc081098, #rx_mobile_team, #kotlindev #androiddev, #rxandroid, #jetpack_compose, #value_class

When using Jetpack Compose, we use Dp type to represent a dimension value representing device-independent pixels (dp). It is used many times and everywhere in Jetpack Compose.

Do you have the same question as me 😇, when looking Jetpack Compose source code: Why do @Composable functions in Jetpack Compose usually use Dp.Unspecified as default value? Why not use null?

Dp.Unspecified is usually used as default value in Jetpack Compose source code

Dp.Unspecified is usually used as default value in Jetpack Compose source code

I. Dp class

I.1. Dp class declaration

At the moment, Dp class is defined as follows:

@Immutable
@kotlin.jvm.JvmInline
value class Dp(val value: Float) : Comparable<Dp> {
    @Stable
    inline operator fun plus(other: Dp): Dp = ...

    @Stable
    inline operator fun minus(other: Dp): Dp = ...

    // [...]
    // Other operators ...
    // [...]

    @Stable
    override /* TODO: inline */ operator fun compareTo(other: Dp) = value.compareTo(other.value)

    @Stable
    override fun toString() = if (isUnspecified) "Dp.Unspecified" else "$value.dp"

    companion object {
        @Stable
        val Hairline = Dp(value = 0f)

        @Stable
        val Infinity = Dp(value = Float.POSITIVE_INFINITY)

        /**
         * Constant that means unspecified Dp
         */
        @Stable
        val Unspecified = Dp(value = Float.NaN)
    }
}

Dp class is declared as inline value class wrapping a Float value. When compiling, the compiler will try to replace all Dp types with Float types (float in Java) as much as possible. This reduces memory usage and improves performance since heap allocation is eliminated, and Float type, a primitive type, is usually heavily optimized by the runtime. That is the optimization of Jetpack Compose.

The inline class spec is here.

I.2. Example ✍️

Let's create a simple example to see how the compiler optimizes Dp class, I create a @Composable fun MyComposable1 accepting a Dp as the first parameter.

MyComposable1 accepts a Dp as the first parameter

MyComposable1 accepts a Dp as the first parameter


After compiling Kotlin and decompiling Java bytecode to Java code, we can see that the compiler has replaced Dp types with Float types.

Dp types are replaced with Float types

Dp types are replaced with Float types


You may notice that the method name MyComposable1 has been changed, that is called mangling.

Mangling: since inline classes are compiled to their underlying type, it may lead to various obscure errors, for example unexpected platform signature clashes. To mitigate such issues, functions using inline classes are mangled by adding some stable hashcode to the function name.

II. Why do @Composable functions in Jetpack Compose usually use Dp.Unspecified as default value? Why not use null?

II.1. Why not use null?

After looking at the Dp class, we can understand why Dp is declared as inline value class, and the optimization of Jetpack Compose under the hood. Let's get back to the question at the beginning of this article.

Dp.Unspecified is usually used as default value in Jetpack Compose source code

Dp.Unspecified is usually used as default value in Jetpack Compose source code


Inline value class and Auto Boxing are keywords here.

"Boxing" refers to converting a primitive value into a corresponding wrapper object. Because this can happen automatically, it's known as autoboxing. Similarly, when a wrapper object is unwrapped into a primitive value then this is known as unboxing. (https://www.baeldung.com/java-wrapper-classes#autoboxing-and-unboxing)

If we use null as default value, we must declare the type as Dp? (nullable type). Since Dp? is a nullable type, compiler cannot use Float (aka float in Java) type to replace Dp? type. Instead, Dp? must be used!!!

nullable inline class types over primitive types are mapped to the boxed reference type (wrapper of an inline class)

Dp? is a reference type, not a primitive type, it requires heap allocation. Imagine we use Dp? types in many places (the worst case is in a loop or is in List/Row/Column/... UI), it will cause a lot of heap allocation, affect the application performance ⚠️, especially a UI application, its performance is very important to the user experience.

II.2. Demonstration example ✍️

Let's create a simple example using Dp? type, we create a @Composable fun MyComposable2 accepting a Dp? as the first parameter.

MyComposable2 accepts a Dp? as the first parameter

MyComposable2 accepts a Dp? as the first parameter


After decompiling, we can see that Dp type is not replaced with Float type, it is preserved. Inside UseMyComposable2, Dp.box-impl(Dp.constructor-impl((float)$this$dp$iv)) is used to box a Float value to a Dp. It is a heap allocation ⚠️.

Dp type is not replaced with Float type and heap allocation is used

Dp type is not replaced with Float type and heap allocation is used


That is the reason why we must not use null as default value. Dp.Unspecified is a better choice ✅, it wraps a Float.NaN value. Sometimes, we can use Dp.Hairline (aka 0.dp) as default value, depending on your use case.

II.3. Bonus ✅

  • How about elvis operator ?:?
  • How about == null?

Jetpack Compose has replacements, they are Dp.takeOrElse(block: () -> Dp): Dp and Dp.isUnspecified: Boolean. takeOrElse is an inline function, no lambda allocation, no heap allocation here 🥰.

Dp.takeOrElse and Dp.isUnspecified

Dp.takeOrElse and Dp.isUnspecified


Thanks for reading 🤗.

Follow me (@hoc081098) and us (https://rx-mobile-team.github.io/profile/) to have more knowledge about programming, not only limited to Mobile (Android/iOS/Flutter) but also Functional Programming, Reactive Programming, Data Structures, Algorithms, ...