Using Java Reflection with Kotlin Companion Objects

Kotlin companion objects allow you to add static data and methods associated with a class. This is similar to how Java has static fields and methods. The problem is that Java doesn’t really know what a companion object it, so trying to access one using standard Java reflection might make you go crazy. 🤪

package com.handstandsam

/** This is contrived example of a companion object */
class SpecialFeature {
  companion object {
    var enabled: Boolean = false
  }
}

The decompiled Java class (created by the Kotlin Compiler) results in this:

package com.handstandsam;

import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
import org.jetbrains.annotations.NotNull;

public final class SpecialFeature {
   private static boolean enabled;

   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      public final boolean getEnabled() {
         return SpecialFeature.enabled;
      }

      public final void setEnabled(boolean var1) {
         SpecialFeature.enabled = var1;
      }

      private Companion() {
      }

      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

The Kotlin Standard Lib for Java has a really cool method called companionObjectInstance that allows you to grab an instance of the declared companion object from the KClass object.

Why is companionObjectInstance helpful?

When Kotlin is compiled to Java Class files, the companion object has a fully qualified class name of com.handstandsam.SpecialFeature$Companion.

Mapping Kotlin -> Java Byte Code can make your head hurt, so by using this companionObjectInstance helper method, we don’t have to figure out how to get an instance of the companion object, or figure out the fully qualified class name.

val companionObjectJavaClass = com.handstandsam.SpecialFeature::class.java
val companionObjectInstance = companionObjectJavaClass.kotlin
        .companionObjectInstance!!

Now that we have an instance of the companion object class, and know the Java class, we can use reflection to set the value of the enabled property on the companion object.

companionObjectInstance::class.java
    .methods
    .first { it.name == "setEnabled" }
    .invoke(companionObjectInstance, true)

Note: setEnabled is the name, and it is a method here. You might expect this to just be a property which is what I assumed, but when compiled to java byte code, it is marked private and has a getter and a setter.

Bonus: Accessing private properties using Java Reflection

You could alternatively use Java reflection to change the backing private static boolean enabled field directly if you choose.

If you wanted to set the private static field value itself, rather than calling the setter, you can grab the declared field, and set it to accessible which allows us to bypass the private visibility. This sort of thing is why the JVM can’t be considered secure as it can be modified at runtime.

val privateEnabledField = SpecialFeature::class.java.getDeclaredField("enabled")
privateEnabledField.isAccessible=true
privateEnabledField.set(companionObjectInstance, true)

Conclusion

Reflection is powerful, but confusing. I could have probably done this cleaner JUST using Kotlin Reflect and not Java Reflection, but in my case I wanted to use Java Reflection, but needed to interact with a Kotlin companion object. There is a lot of documentation on how to mix Kotlin + Reflection, so feel free to read up more there. Cheers!

Kotlin Sealed Interfaces with KotlinX Serialization JSON

I heavily use sealed interfaces to model result objects in Kotlin as they allow me to create a type of classes that can be handled using exhaustive when statements, similar to an enum, but also each type can contain its own properties.

I wanted to serialize these sealed interface Kotlin models to/from JSON over HTTP. There are a bunch of options for serializing JSON in Java like Moshi, Gson and Jackson. While all of those libraries are great, I had a requirement of creating a multi-platform library, and went with KotlinX Serialization.

In this post I’ll walk you through an example of how I configured KotlinX Serialization to work for my use case.

Example: Marketing Campaigns API Result

This endpoint returns a strongly typed campaign, and I wanted to represent this in JSON.

public sealed interface CampaignContent {
    public data class PopupModal(
        public val imageUrl: String,
        public val text: String,
        public val subtext: String,
    ) : CampaignContent

    public data class Link(
        public val linkText: String,
        public val url: String,
        public val linkIcon: String? = null,
    ) : CampaignContent
}
{
  "type": "popup_modal",
  "image_url": "https://...",
  "text": "Text",
  "subtext": "Subtext"
}
{
  "type": "link",
  "link_icon": "https://...",
  "url": "https://..."
}

We need to deserialize a JSON response into a strongly typed object that implements the CampaignContent sealed interface.

fun getCampaignContentFromServer() : CampaignContent

KotlinX Serialization has Polymorphism support allows us to do this. You need to register polymorphic definitions in a SerializersModule that you provide to your Json object that is used to encode and decode objects to/from JSON.

val jsonSerializer = Json {
  serializersModule = SerializersModule {
    polymorphic(
      CampaignContent::class,
      CampaignContent.PopupModal::class,
      CampaignContent.PopupModal.serializer(),
    )
    polymorphic(
      CampaignContent::class,
      CampaignContent.Link::class,
      CampaignContent.Link.serializer(),
    )
  }
}
val campaignContent : CampaignContent = jsonSerializer.decodeFromString(
  CampaignContent.serializer(), 
  jsonString,
)

In order to support polymorphism, a type property is used in the JSON string representation {"type": "..."}. By default this "type" field is a fully qualified classname. This allows KotlinX Serialization know what type to deserialize. You have control over what the name of this classDiscriminator field is, as well as other configuration options when configuring your Json {} serializer.

If you don’t want to use the fully qualified class name as the class type, then you can put a @SerializedName("...") annotation to the class and it will use that name instead of the fully qualified class name. This is helpful for me as the backend did not use fully qualified names, and I had set them explicitly. In the example below I added the @SerializedName("popup_modal") data class.

Final Models after adding @Serializable and @SerializedName

public sealed interface CampaignContent {

  @Serializable
  @SerializedName("popup_model")
  public data class PopupModal(
    @SerializedName("image_url")
    public val imageUrl: String,
    @SerializedName("text")
    public val text: String,
    @SerializedName("subtext")
    public val subtext: String,
  ) : CampaignContent

  @Serializable
  @SerializedName("link")
  public data class Link(
      @SerializedName("link_text")
      public val linkText: String,
      @SerializedName("url")
      public val url: String,
      @SerializedName("link_icon")
      public val linkIcon: String? = null,
  ) : CampaignContent
}

Considerations

At first I made my models match the JSON values as I didn’t have to specify @SerializedName since KotlinX Serialization will just match the field name. After a bit of usage, link.link_text just didn’t feel as correct as link.linkText, so I chose to specify a @SerializedName annotation instead. The resulting Java bytecode is the same as the KotlinX Serialization plugin does code generation that writes out the serializer anyways. This does make your data class look not as pretty, but from the general building and usage perspective of these models, the user will not know.

Conclusion

That was a whirlwind intro, but I had to really dig through deep into the documentation to figure it out and am hoping this helps someone do this faster than I did it originally.

[Experiment] Espresso Closed-Box Testing

I wanted to write some Android Espresso tests for a large application, but iterate on the tests as fast as possible.

Typically someone would run :app:connectedDebugAndroidTest to run their instrumentation tests, but under the hood that is just compiling and installing both the app and androidTest apks, and using the instrumentation runner over adb.

When executing Android Instrumentation Tests, you just need an app.apk and an androidTest.apk, and then to invoke the test instrumentation runner via adb.

Because of the configuration, the androidTest APK gets everything that is on the app‘s classpath so it can reference resources, classes and activities in the app.

The Experiment

I wanted to see if I could build an androidTest.apk without having any ties to the original :app. I tried a few methods, but found that creating a new blank application with the exact same package name, and then writing tests under the androidTest folder allowed me to compile quickly.

Problems:

  1. No access to the classpath & resource identifiers
  2. Classpaths can’t clash (must use same versions of dependencies as the original app).

Workarounds:

  1. You could import just a few modules that have resource identifiers or code that you want to reference in your tests. (easier and typesafe, but a little slower)
  2. OR you could just access everything by fully qualified package names, and look up resource identifiers by ID. (no compile time safety, but faster)

I tried workaround #2, because I wanted to have this be the fastest iteration time possible, and I finally got it to work! Here’s my receipt for how I made it happen.

How I Got it Working

1) Install my app (com.example.app) as usual :app:installDebug.

This will be the app I want to test.

2) Create the :cloneapp project

In this :cloneapp project, keep an empty main source folder, but add an androidTest directory.

3) In :cloneapp set the package name to the the exact same package name com.example.app.

android {
    defaultConfig {
        applicationId "com.example.app"
    }
}

4) In :cloneapp update the src/androidTest/AndroidManfest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:tools="http://schemas.android.com/tools">
    <instrumentation
        android:name="androidx.test.runner.AndroidJUnitRunner"
        android:targetPackage="com.example.app"
        android:targetProcesses="com.example.app" />
</manifest>

5) Add in a test!

package com.example.app.tests

import android.app.Activity
import android.content.Context
import android.os.SystemClock
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test

fun findResourceIntByIdStr(id: String): Int {
    ApplicationProvider.getApplicationContext().resources.getIdentifier(id, "id", applicationContext.packageName)
    Espresso.onView(ViewMatchers.withId(findResourceIntByIdStr(idStr)))
}

fun findViewByIdStr(idStr: String): ViewInteraction {
    Log.d(TAG, "Find View By ID Str $idStr")
    return 
}

class ExampleTest {

    /** Use this to interact with Compose surfaces */
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testLoginFlow() {
        
    }
}

6) Install the test clone APK

Run :cloneapp:installDebugAndroidTest to install the test apk.

7) Run the tests using adb!

adb shell am instrument -w -r com.example.app.test/androidx.test.runner.AndroidJUnitRunner

Note: You can be more explicit with command line instrumentation arguments about what test or test class you want to execute.

8) Test Development Iteration Loop

I ended up clearing the app data between runs with adb shell pm clear com.example.app as well so I had consistent behavior and didn’t have to install the package.

Conclusion

As mentioned, this was an experiment. It made the iteration time blazing fast, but lacked compile time safety. Anyways, it’s possible, and hopefully you learned something. If you end up using this technique, I’m curious to hear more. Feel free to message me on Kotlin Lang Slack or Mastodon.

Organizing @Composables

I saved this post as a draft on February 23, 2021 and never published it. Almost 2 years later this topic came up again, so I want to publish it as a current post to create discussion on the topic. Feel free to ping me on Mastodon with your thoughts and feedback.

Jetpack Compose for Android is AMAZING, and I’m so excited for it to be stable and the recommended way to build Android applications. I’ve been working with the alpha versions of Compose 1.0.0 on Android for the last 6 months on side projects and have been doing a lot of thinking based on my experiences. One thing that came to mind when I first started, and still does, is how to structure my project and organize all my @Composable functions.

I’ve asked this question for #TheAndroidShow which is tomorrow, and hopefully will get some recommendations, but I’ve already had some good conversations on Twitter.

When I look around at the AndroidX Compose Library (Button.kt) and the compose-samples that the Google Developer Relations Team has published, I see that composables are organized by file.

Over the last 4 years of being a full-time Kotlin developer, I have tried to keep a single class or object per file. I also try to avoid writing code at the root level of a file.

The only exceptions I make when writing Kotlin code are for extension functions and typealiases, since those are required to be written directly in a file. I’m used to finding items by what class/object they are in, and not the function name. I’ve also historically found that large refactoring has been more successful when a file has a single Kotlin class or object.

For this post, I made up the GreetingHeader and GreetingContent @Composables for the purpose of having an example of related @Composables.

Let’s take a look at some options on how to group related @Composable functions.

Root Level in a File

@Composable
fun GreetingHeader() {
    Text(
        text = "Hello",
        style = MaterialTheme.typography.h1
    )
}

@Composable
fun GreetingContent() {
    Box(contentAlignment = Alignment.Center) {
        Text(
            text = "How are you?",
            modifier = Modifier.padding(16.dp)
        )
    }
}

Top level functions for nice clean syntax, but how do you find these composables in a project you are unfamiliar with? Composables don’t extend other classes or implement interfaces, so it’s hard to use built in tools to Android Studio and Intellij to find related ones.

In an object

object Greetings {
    @Composable
    fun GreetingHeader() {
        Text(
            text = "Hello",
            style = MaterialTheme.typography.h1
        )
    }

    @Composable
    fun GreetingContent() {
        Box(contentAlignment = Alignment.Center) {
            Text(
                text = "How are you?",
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

I originally leaned on this so I could search for a @Composable, and use auto complete. I do like it, but I don’t like the longer composable names, even though I can do static imports.

In a class

class Greetings {
    @Composable
    fun GreetingHeader() {
        Text(
            text = "Hello",
            style = MaterialTheme.typography.h1
        )
    }

    @Composable
    fun GreetingContent() {
        Box(contentAlignment = Alignment.Center) {
            Text(
                text = "How are you?",
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

I haven’t tried this yet. It could work though (it compiled), especially if you want to be able to access injected dependencies, etc.

In a class or object, via an interface

interface HeaderAndContent {
    @Composable
    fun Header()

    @Composable
    fun Content()
}

object Greetings : HeaderAndContent {
    @Composable
    override fun Header() {
        Text(
            text = "Hello",
            style = MaterialTheme.typography.h1
        )
    }

    @Composable
    override fun Content() {
        Box(contentAlignment = Alignment.Center) {
            Text(
                text = "How are you?",
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

I haven’t used an interface yet, but it’s an interesting thought. I checked it out and it’s possible and compiles. It could help with discoverability for similar @Composables.

This also gets me thinking about capitalization of @Composables. It isn’t required from a compiler standpoint, so what if we did this.

interface HeaderAndContent {
    @Composable
    fun header()

    @Composable
    fun content()
}

object Greetings : HeaderAndContent {
    @Composable
    override fun header() {
        Text(
            text = "Hello",
            style = MaterialTheme.typography.h1
        )
    }

    @Composable
    override fun content() {
        Box(contentAlignment = Alignment.Center) {
            Text(
                text = "How are you?",
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

Conclusion

It is very early on, and I’m not sure what the best way will be to organize @Composable functions. I typically say to do what the community is doing, but at this point we just have what the Google team is doing really. Let’s all use Compose some more, and figure out what works the best, but keep an open mind and try new things. Feel free to reach out with what you’re trying on Mastodon.

Debugging Android Intents

With new behaviors for apps using targetSdk=33 (Android 13) regarding Intents, it may be necessary to dive in and figure out how to make things compatible.

In doing this myself, I needed to figure out what was in the Intent, so I could handle it appropriately.

I started with this StackOverflow post, but ended up adding more info and doing it cleanly in Kotlin.

fun Intent?.toDebugString(): String {
    val intent = this ?: return ""
    return StringBuilder().apply {
        appendLine("--- Intent ---")
        appendLine("type: ${intent.type}")
        appendLine("package: ${intent.`package`}")
        appendLine("scheme: ${intent.scheme}")
        appendLine("component: ${intent.component}")
        appendLine("flags: ${intent.flags}")
        appendLine("categories: ${intent.categories}")
        appendLine("selector: ${intent.selector}")
        appendLine("action: ${intent.action}")
        appendLine("dataString: ${intent.dataString}")
        intent.extras?.keySet()?.forEach { key ->
            appendLine("* extra: $key=${intent.extras!![key]}")
        }
    }.toString()
}

Use the extension function above with println(myIntent.toDebugString()).

You can then filter Logcat with “System.out” and see the results! I hope this helps someone figure out what’s inside your Intents!

Using the `kotlin-dsl` Gradle Plugin forces Kotlin 1.4 Compatibility 😱

I previously wrote an article “Sharing Gradle Configuration in Multi-Module Android Projects” about re-using Gradle configuration using the “apply” feature to reduce boilerplate and provide consistency. It’s super helpful and handy, but there is now a better way to do the same kind of thing using Gradle Convention Plugins.

Gradle Convention Plugins

There is a great post by Tony Robalik that goes into the benefits of Gradle Convention Plugins. I say it is “better” because it can be pre-compiled, written in Kotlin and tested in Kotlin. These convention plugins are most easily added in buildSrc, so I figured I’d start there when adding it to my existing project.

My journey writing a Gradle Convention Plugin and how I ran into Kotlin 1.4

So, in a large Android project I added the “kotlin-dsl” plugin to the buildSrc module and things blew up. I’m using the latest version of Gradle 7.4.2, yet it is telling me:

Language version 1.4 is deprecated and its support will be removed in a future version of Kotlin

I had specified Kotlin 1.6.10 everywhere! What was I doing wrong?

Nothing. It’s intentional. Gradle even calls out why on their site:

Gradle plugins written in Kotlin target Kotlin 1.4 for compatibility with Gradle and Kotlin DSL build scripts, even though the embedded Kotlin runtime is Kotlin 1.5.

https://docs.gradle.org/current/userguide/compatibility.html#kotlin

Even though it is intentional, it wasn’t immediately clear to me, and to others, but I understand the thinking behind it now, and in this post show you how you can get around it if you need to.

Martin Bonnin has a great post talking about how you could get around this by doing some crazy things like creating shadow jars, but his answer of “Should I use this in production?” was “it depends”, and where possible, I try to not use complex workarounds (even though this post is a less complex workaround 😂).

Gradle 7.4.2 still targets Kotlin 1.4 with the kotlin-dsl plugin, even though 1.5.31 is embedded now in Gradle 7.4.2. Gradle plugin compatibility is very important when distributing plugins publicly, but if you are just going to use them in your team or organization, you may not want to support old version of Kotlin going back to 1.4.

In order to use the version of Kotlin available in the version of Gradle you have, you need to specify the version yourself to override the default.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    `kotlin-dsl`
}

afterEvaluate {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            apiVersion = "1.5"
            languageVersion = "1.5"
        }
    }
}

This will allow you to use Kotlin 1.5 syntax now when using the “kotlin-dsl” plugin!

One thing that is weird about this is the need for afterEvaulate. I’m not 100% sure why it was needed, but it’s how they specify it in Gradle source code, and it doesn’t work without adding afterEvaluate.

But… what about Kotlin 1.6?

If you really want to use Kotlin 1.6 though, fear not! Kotlin 1.6.10 is going to be available with Gradle 7.5 when it comes out. When it does, you should be able to use this configuration to use 1.6 compatibility:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    `kotlin-dsl`
}

afterEvaluate {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            apiVersion = "1.6"
            languageVersion = "1.6"
        }
    }
}

Kotlin/Gradle Version Table when using kotlin-dsl

Gradle VersionEmbedded Kotlin VersionDefault Api Version
7.5 (Unreleased as of April 14, 2022)1.6.101.4
7.3+1.5.311.4
7.0+1.4.301.4

What do you recommend?

If you are only going to use this plugin internally, and everyone is using the same version of Kotlin everywhere, this seems pretty safe. If you are looking to open source something for public use, you may need to call out a minimum Gradle version required use your Plugin.

Should I put my Gradle Convention Plugin in buildSrc?

This is the first place you should try it out. You will have the same issue with any code you put in buildSrc though, where if any code changes, all code and tests in buildSrc have to be re-run which increases build times.

If you plan to use this a lot going forward, use an includeBuild to add in your Convention Plugins going forward so that you only re-compile when that code changes, and so you could publish a binary to avoid any compilation at all.

Reviewers

Thanks Martin Bonnin and Tony Robalik for reviewing the article. Also special thanks to Martin for helping me dig into the Gradle source to figure this out!

Report of your Android App’s Permissions

Permission Awareness

Knowing what you ship to your users is key, and permissions are one part of it. If you just look at your src/main/AndroidManifest.xml file, and think that’s all the permissions you will be shipping to the Play Store, you may find your self surprised that it may not be true. Applications can declare permissions, but 3rd party libraries and modules can declare them as well. Only the final, AndroidManifest.xml that has been generated via the manifest merging process is the source of truth.

In this article I share how the merged manifest is created, where you can find it, and a small Python script I created to parse the file and print out a list of permissions.

What is the “manifest merge” process?

There is a “manifest merge” process which takes all libraries and modules that your application relies on which ends up generating the final AndroidManifest.xml file. Merging the manifest is important because dependencies you rely on can bring in extra permissions you don’t define in your application yourself.

Where do I get my merged AndroidManifest.xml file?

Option 1: Grab the merged manifest from the build directory.

  • Build your APK via Android Studio or the command line.
    • Example ./gradlew app:assembleRelease
  • Locate your merged AndroidManifest.xml in the build directory: app/build/intermediates/merged_manifests/release/AndroidManifest.xml

Option 2: (You only have the APK, but didn’t build it yourself)

  • Copy/paste the APK you have into Android Studio. Then open it, and view the AndroidManifest.xml. At that point, you can view it there, or copy out the contents to use with the script.
  • Alternatively you can use something like apktool if you don’t have Android Studio.

How do I view all the permissions?

If you are looking to just see if a single permission has been declared, just search through the large AndroidManifest.xml file.

How do I create a permission report?

Sometimes it is nice to know what all your permissions are in a nice clean way. There are thousands of ways you could do this, but I created a python script that creates a clean alphabetized list of all your permissions.

Example Output

android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_WIFI_STATE
android.permission.CAMERA
android.permission.FOREGROUND_SERVICE
android.permission.INTERNET
android.permission.VIBRATE

Instructions

  • Download print_permissions_from_androidmanifest.py and place it in a directory.
  • Open your APK in Andorid Studio (by double clicking on it in your build folder)
  • Copy the contents of AndroidManifest.xml to the clipboard, and save it to a file named AndroidManifest.xml in the same directory as print_permissions_from_androidmanifest.py.
  • Run python print_permissions_from_androidmanifest.py

Conclusion

It’s nice to cleanly see what permissions you are requesting when you send your app. There are probably better ways to do something like this, and if you know of some, I’ll be happy to link them in this post!

Install Referrer Kotlin Extension

There isn’t a Kotlin Extension (KTX) library for Google Play’s InstallReferrer Library, so I wanted to share how I wrapped the API in a very Kotlin friendly way.

The Install Referrer API allows you to securely retrieve referral content from Google Play.  This can help you understand how you are acquiring new users in your app, and help you provide customized experiences tailored to where a user came from. For instance, if someone downloaded the app on a “New York” section of your website, you could open the “New York” section of the app on first app launch.

I hope this wrapper helps you!

https://gist.github.com/handstandsam/686a1bb551d0426b51dd612890f64986

Introducing “Saydle 🔊” – A Real-time Gradle Audio Notification Script for Mac OS

Saydle 🔊 is a Wrapper for the Gradle Wrapper (gradlew) that notifies you via the Mac OS say command whether a task failed or succeeded.

Why?

Knowing when a Gradle build is done is super helpful when there are long running tasks. This allows you to be notified instead of checking the terminal.

https://twitter.com/HandstandSam/status/1463207029007339520

How does it work?

  • Instead of using ./gradlew, use ./saydlew instead. That’s it!
  • Example: ./saydlew app:assembleDebug

Try It Out

Check out the install instructions on GitHub.

Saydle 🔊 on GitHub

Feedback?

Let me know what you think on Twitter at @Handstandsam, or report issues on GitHub.

Jetpack Compose – Text Shadows

This post is a journey of the steps I took while figuring out how to do a Text Shadow with Jetpack Compose. If you want to skip the journey and just get the solution, jump to the end of the post!


As of version 1.0 of Jetpack Compose, Text Shadows don’t exist in the same way they used to on TextView. 😿

Adding a Shadow on a TextView looked like this:

<TextView
    android:id="@+id/text"
    style="@style/CategoryRowTitle"
    tools:text="Category" />
<style name="CategoryRowTitle" parent="TextAppearance.AppCompat">
    <item name="android:textSize">24sp</item>
    <item name="android:textColor">@color/white</item>
    <item name="android:shadowColor">@color/black</item>
    <item name="android:shadowDx">4</item>
    <item name="android:shadowDy">4</item>
    <item name="android:shadowRadius">8</item>
</style>

Adding a Shadow to Text in Jetpack Compose

You try can put a “shadow” on your Text Composable, but it’ll create a shadow behind the text container, not the actual characters. 🤔

Text(
    text = "Fruits",
    modifier = Modifier
        .shadow(elevation = 2.dp)
)

Creating a Custom Shadow in Jetpack Compose

I did my best to create a shadow myself by making a copy of the text, setting it to a dark color, and offsetting it by 2.dp.

@Composable
fun TextWithShadow(
    text: String,
    modifier: Modifier
) {
    Text(
        text = text,
        color = Color.DarkGray,
        modifier = modifier
            .offset(
                x = 2.dp,
                y = 2.dp
            )
            .alpha(0.75f)
    )
    Text(
        text = text,
        color = Color.White,
        modifier = modifier
    )
}

Looks great! But small differences.

Let’s Cheat and Use an AndroidView in Compose 😃

Compose has amazing interoperability with the Android View system. If something isn’t perfect in Compose, we can always just use the Android View version. Pixel perfect match! However, this wouldn’t work in Compose for Desktop because it keeps us tied to the Android View system.

@Composable
fun ComposeAndroidTextView(
    text: String,
    modifier: Modifier
) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            AppCompatTextView(context).apply {
                setTextAppearance(R.style.ItemRowTitle)
                this.text = text
            }
        }
    )
}

Let’s Try Again with Compose… StackOverflow? 🤔

I did find this Stack Overflow post that was similar, but not exactly what I needed. Here is what it had:

val textPaintStroke = Paint().asFrameworkPaint().apply {
    isAntiAlias = true
    style = android.graphics.Paint.Style.STROKE
    textSize = 64f
    color = android.graphics.Color.BLACK
    strokeWidth = 12f
    strokeMiter = 10f
    strokeJoin = android.graphics.Paint.Join.ROUND
}

val textPaint = Paint().asFrameworkPaint().apply {
    isAntiAlias = true
    style = android.graphics.Paint.Style.FILL
    textSize = 64f
    color = android.graphics.Color.WHITE
}

Canvas(
    modifier = Modifier.fillMaxSize(),
    onDraw = {
        drawIntoCanvas {
            it.nativeCanvas.drawText(
                "Sample",
                0f,
                120.dp.toPx(),
                textPaintStroke
            )
            it.nativeCanvas.drawText(
                "Sample",
                0f,
                120.dp.toPx(),
                textPaint
            )
        }
    }
)

What’s the Perfect Way to Match a TextView Shadow with Compose?

I’m not really sure. Update: I figured it out thanks to Antonio Leiva!

style = MaterialTheme.typography.h4.copy(
    shadow = Shadow(
        color = shadowColor,
        offset = Offset(4f, 4f),
        blurRadius = 8f
    )
)

I wanted to share my journey in figuring this out, but also thank everyone in the community for helping find the “right” way to do it in compose!