Patch Gaps in Current Compose API Using Reflection | by Jamal Mulla

Fix a small niggle in Jetpack Compose

Photo by Serge Kutuzov on Unsplash

Recently, I decided to redesign an old Android app with Jetpack Compose. The new Compose system is a significant improvement over the previous View system but it is still rather new and there are missing features. One such gap I came across was in the OutlinedTextField composable in the androidx.compose.material library which doesn’t offer a way to modify the default width of the outline. Of course, this isn’t a big deal but it didn’t fit the overall theme of my app so I decided to use reflection to fix this little niggle anyway. I’ve documented my approach and the final function which I call once at app launch to make the change.

There are many arguments both for and against using reflection which I won’t go in to here, but trivial uses such as the one I’m describing here aren’t dangerous and do not have any significant runtime performance costs. Of course, if there is an alternative way of achieving the same behaviour, prefer that but in this case there isn’t as OutlinedTextField is part of a library.

We will be talking about reflection in the context of the JVM platform but executed on Android using Kotlin and Java.

Simply put, reflection allows us to introspect our programs at runtime and to modify properties, fields, methods and other language constructs. This includes not only public properties but also fields marked internal or private.

Although Kotlin has its own additional functionality for working with reflection, we will be using the standard Java reflection functions instead for this particular change as Kotlin specific reflection is still rather limited at the time of writing and the change we want to make isn’t actually possible. Of course, this isn’t an issue as Kotlin interoperates with Java perfectly.

So we want to modify the width of the text field outline. The first thing to do is find where this width is actually set. If we follow the definition of OutlinedTextField we soon come across androidx.compose.material.TextFieldImplKt.class which contains the JVM byte-code for TextFieldImpl.

Of course, if you’re viewing this in Android Studio and you have everything set up correctly, you should be able to see the source directly. We can have a quick look over the code and we see that IndicatorUnfocusedWidth and IndicatorFocusedWidth are private variables defined at the bottom of the file.

private val IndicatorUnfocusedWidth = 1.dp
private val IndicatorFocusedWidth = 2.dp

I want the unfocused width to be the same as the focused width so that I get a consistent line width throughout. In my case, 2.dp is perfect so I will be taking a slight shortcut by setting the unfocused width to the focused width.

If you want to set the values ​​to whatever you want, bear in mind that these variables are actually floats and trying to set a dp value directly will not work. This can be confirmed by looking at the bytecode directly. If we look at the Smali produced for TextFieldImplKt we see these two lines:

.field private static final IndicatorFocusedWidth:F 
.field private static final IndicatorUnfocusedWidth:F

showing that they are compiled down to private static fields of type Float (that’s the F at the end).

We can now start by getting a reference to the class we’re modifying. As we saw above this is TextFieldImplKt :

val textFieldImpl = Class.forName("androidx.compose.material.TextFieldImplKt")

Now, we need to get the fields. We can do this by using the getDeclaredField() method:

val indicatorUnfocusedWidth = textFieldImpl.getDeclaredField("IndicatorUnfocusedWidth")
val indicatorFocusedWidth = textFieldImpl.getDeclaredField("IndicatorFocusedWidth")

Although we now have the fields we want to modify, we cannot set them just yet. This is because the standard modifiers are still in place and we saw above that these fields are marked private . Trying to read or write our fields at this point will result in an IllegalAccessException . We need to make these fields accessible first:

indicatorUnfocusedWidth.isAccessible = true
indicatorFocusedWidth.isAccessible = true

Now we can finally set these to whatever we want. To set the unfocused width to the focused width we can simply do:

indicatorUnfocusedWidth.set(null, indicatorFocusedWidth.get(null))

As both these fields are static, there is no associated object which means we can just pass null to both of these parameters. If the field you’re modifying is a class member, then you will need to pass in the object for which this property should be changed.

If you want to set a dp value directly, you can use .value like this:

indicatorUnfocusedWidth.set(null, 3.dp.value)

The full function with a bit of basic exception handling:

You can call this somewhere once at the beginning when your app launches, and the change will be permanent during execution. This is a rather safe use of reflection as the only consequence of a failure to make the change you want will be a normal outline width. Additionally, the function is called just once so the typical performance costs associated with reflection become irrelevant.

The result is subtle but it does make a noticeable difference in my eyes.

Before and after changing the size of the TextInput outline

This article has demonstrated how reflection can be used to subtly modify the UI in an Android app but the technique shown here can be used for many other modifications, more complicateds. That said, I wouldn’t use reflection for anything too complicated as there is a performance impact.

Use it for subtle things which won’t have any real effect if the modification fails and which you only have to call a few times at most. Finally, always wrap your changes in a try/catch block to ensure you don’t crash your app.

The example this article uses, OutlinedTextField, has changed the way it draws its outline since this article was written. The method above is still valid but the border thickness is now defined in TextFieldDefault.kt as UnfocusedBorderThickness. It’s also now an actual option in TextFieldDefault.BorderBox so it should be easier to use without using reflection.

Originally, I also didn’t mention the changes needed to your Proguard rules for this to work. If you use Proguard/R8 to obfuscate your app, most, if not all classes will lose their names. If you use reflection, this isn’t what you want as you have to find the class you want to modify by name at runtime. Therefore, you must add a rule such as the following for all the classes you’ll refer to by name:

-keepclasseswithmembernames class androidx.compose.material.TextFieldImplKt {
public <methods>;
}

I’m not an expert at Proguard so there may be a better rule for this but this has worked for me in the past.

Leave a Comment