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.