Conquer Composable Functions in Jetpack Compose: Stop Struggling with Android Modern UIs

Table of Contents

When I first started exploring Jetpack Compose, I found it both fascinating and challenging. It was a whole new way of building UI, replacing XML layouts with a declarative approach. Today, I’m going to share my journey of understanding Composable Functions, the building blocks of Jetpack Compose. Together, we’ll demystify them, explore how they work, and write some Kotlin code to see them in action.

Let’s start by dissecting the anatomy of a composable function.

Anatomy of a Composable Function

Basically, in Jetpack Compose, the fundamental building block for creating a UI is called a composable function. Which is annoted with @Composable annotation, Let’s break down what it is and how it works.

First, if you see or find any @Composable annotation, right-click on it and select ‘Go To’ -> ‘Declarations & Usage,’ which will redirect you to the Composable.kt file.

There, you will find some commented documentation and code. We will focus on the code, but first, let’s analyze what the documentation says.

Kotlin
/**
 * [Composable] functions are the fundamental building blocks of an application built with Compose.
 *
 * [Composable] can be applied to a function or lambda to indicate that the function/lambda can be
 * used as part of a composition to describe a transformation from application data into a
 * tree or hierarchy.
 *
 * Annotating a function or expression with [Composable] changes the type of that function or
 * expression. For example, [Composable] functions can only ever be called from within another
 * [Composable] function. A useful mental model for [Composable] functions is that an implicit
 * "composable context" is passed into a [Composable] function, and is done so implicitly when it
 * is called from within another [Composable] function. This "context" can be used to store
 * information from previous executions of the function that happened at the same logical point of
 * the tree.
 */

It says the following about Composable Functions:

  • Composable Functions:
    Functions marked with @Composable are the core components of a Compose UI.
  • Transform Data to UI:
    These functions describe how data should be displayed in a UI hierarchy (tree of UI elements).
  • Implicit Context:
    When you call a composable function, it gets an implicit “composable context.” This context helps Compose keep track of previous function calls and updates efficiently during recompositions.
  • Restriction:
    @Composable functions can only be called from other @Composable functions. You can’t call them directly from non-composable functions.

Now, let’s see how @Composable is defined.

Annotation Declaration

Kotlin
@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
@Target(
    AnnotationTarget.FUNCTION,
    AnnotationTarget.TYPE,
    AnnotationTarget.TYPE_PARAMETER,
    AnnotationTarget.PROPERTY_GETTER
)
annotation class Composable

Have you noticed that the @Composable annotation itself is defined by three other annotations? Let’s break this down further, and see them one by one.

  • @MustBeDocumented: This annotation indicates that the annotated element should be included in the generated documentation (e.g., when using KDoc to generate documentation).
  • @Retention(AnnotationRetention.BINARY): Specifies that the annotation is retained in the compiled bytecode (e.g., .class files) but is not available at runtime (not accessible via reflection).
  • @Target: It defines the types of elements to which the annotation can be applied. For example, the @Composable annotation can be applied to functions and properties, indicating that these are composable functions in Jetpack Compose. In short, it specifies where the @Composable annotation can be applied. 

It can be used on:

  • Functions (AnnotationTarget.FUNCTION):
Kotlin
@Composable fun MyComponent() { /*...*/ }


//Example

@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}
  • Types (AnnotationTarget.TYPE):
Kotlin
fun <T : @Composable () -> Unit> someFunction() { /*...*/ }



// Example 

// A composable function that takes no parameters and returns Unit
@Composable
fun Greeting(name: String) {
    // This is a composable that displays a greeting
    Text(text = "Hello, $name!")
}

// A generic function that takes a composable function as a parameter
fun <T : @Composable () -> Unit> someFunction(composable: T) {
    // Here you can invoke the composable function
    composable()
}

@Preview
@Composable
fun PreviewSomeFunction() {
    // Passing the Greeting composable function as a parameter to someFunction
    someFunction { Greeting("Compose") }
}
  • Property Getters (AnnotationTarget.PROPERTY_GETTER):
Kotlin
val isEnabled: Boolean
  @Composable get() { return true }


//Another Example 

val greetingText: String
    @Composable get() = "Hello from a composable property!"

One remains. Why did I leave it for last? Because, theoretically, it exists, but practically, it hasn’t been implemented by the Compose team yet. Let’s see what it is in more detail.

  • Type Parameters (AnnotationTarget.TYPE_PARAMETER):
Kotlin
fun <T : @Composable () -> Unit> someFunction() { /*...*/ }



// Example 

// A composable function that takes no parameters and returns Unit
@Composable
fun Greeting(name: String) {
    // This is a composable that displays a greeting
    Text(text = "Hello, $name!")
}

// A generic function that takes a composable function as a parameter
fun <T : @Composable () -> Unit> someFunction(composable: T) {
    // Here you can invoke the composable function
    composable()
}

@Preview
@Composable
fun PreviewSomeFunction() {
    // Passing the Greeting composable function as a parameter to someFunction
    someFunction { Greeting("Compose") }
}

While the annotation declares AnnotationTarget.TYPE_PARAMETER as a valid target, the Compose compiler does not actually support constraining type parameters with @Composable functions. This can lead to issues such as a ClassCastException.

Kotlin
fun <T : @Composable () -> Unit> someFunction(composable: T) {
    composable()
}

What’s wrong? Why does this fail?

  • fun <T : @Composable () -> Unit> someFunction() is not supported by the Compose compiler. The @Composable annotation cannot be applied as a constraint to a type parameter in practice, even though AnnotationTarget.TYPE_PARAMETER exists in the annotation’s definition.
  • Practical Workaround: Instead of using generics, define function parameters directly with @Composable () -> Unit.
Kotlin
// Instead of constraining a type parameter with @Composable, use a regular function parameter

@Composable
fun someFunction(content: @Composable () -> Unit) {
    content()
}

The theoretical declaration of AnnotationTarget.TYPE_PARAMETER for @Composable might indicate future or planned support, but as of now, it’s not usable due to the unique nature of composable functions and how the Compose compiler handles them.

Composable Function Restrictions and Rules

Can Only Call From Other @Composable Functions

A @Composable function can only be called from within another @Composable function or a composable context. This is necessary because @Composable functions require the Compose runtime to manage their lifecycle, track state, and handle recompositions properly.

Kotlin
@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}

fun regularFunction() {
    // This will cause an error!
    Greeting("Jetpack Compose")
}

It will suggest adding @Composable to the regular function, with the message: ‘@Composable invocations can only happen from the context of a @Composable function.’

Implicit Context

When a @Composable function is called, it operates within a composable context. This context allows the Compose runtime to:

  • Track changes to state.
  • Recompose parts of the UI efficiently when the state changes.
  • Manage the lifecycle of composable functions.

This context is automatically provided when you’re within a composable function, making it possible for the Compose framework to determine how and when to re-render parts of the UI.


After diving deep into the world of Composable functions and understanding how they build dynamic and flexible UI components in Jetpack Compose, you might be wondering—How do these Composables actually show up on our screen? 🤔 That’s where setContent() steps in. It’s like the front door to your Composable world, the place where everything begins. In the next part, let’s take a closer look at setContent() and see how it brings your UI to life.

What is setContent?

In traditional Android UI development (using XML layouts), you would typically call setContentView() inside your Activity to set the screen’s layout. With Jetpack Compose, things have changed!

setContent replaces setContentView when you want to define your UI using Compose.

From now on, if you use Jetpack Compose, use this:

Kotlin
// This is defined for illustration purposes. You can use any composition or composable functions inside setContent{...}.

setContent {
    MyComposableContent()
}

Instead of using the older setContentView method with XML layouts.

Basic Syntax Example

Kotlin
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import android.os.Bundle

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello, Jetpack Compose!")
        }
    }
}

How Does setContent Work?

The setContent function is part of the ComponentActivity class. It allows you to set a Composable block as your UI content.

What Happens When You Call setContent?

  1. Initializes the Compose UI framework.
  2. Sets the content view with a root ComposeView that renders your composables.
  3. Observes recompositions to keep the UI up-to-date with state changes.

Essentially, setContent is the entry point for Jetpack Compose to build and display your UI.

Function Signature

Here’s the function definition again for reference:

Kotlin
fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
)

Here,

Extension Function:
setContent() is an extension function for ComponentActivity, which means you can call it on any ComponentActivity or its subclasses like AppCompatActivity.

parent: CompositionContext? = null:

  • This optional parameter represents the parent composition context. In Jetpack Compose, a CompositionContext coordinates updates and re-compositions of composable functions.
  • Passing a parent helps coordinate the lifecycle of this composition with another, which is useful for nested composable hierarchies.

content: @Composable () -> Unit:

  • This parameter is a composable lambda function that defines the UI contents.
  • For Example,
Kotlin
setContent {
    Text("Hello Compose!")
}

Preview

I am going a little out of context, but it’s okay. I feel it’s important for our next discussion. Let’s think: what do you think might be inside the setContent() function body ({..})? 

Kotlin
fun ComponentActivity.setContent(){...}

Does Android still use the old View system at its core, or has it introduced something new? If Android has introduced a new system, how can we still support our old or legacy code written in XML?

The answer is simple: ComposeView. It acts as a bridge between Jetpack Compose and the traditional View system. This allows us to integrate Jetpack Compose with existing XML-based layouts and continue using our legacy code when needed.

Although Jetpack Compose offers a completely new way to build UIs, it does not use the old View system internally for rendering. Instead, Compose has its own rendering mechanism. However, thanks to ComposeView and interoperability APIs like AndroidView, Compose and the View system can seamlessly work together. This is why setContent() can host composables within an activity or fragment, enabling us to mix both approaches in a single app.

It’s just an additional insight. Now, let’s get back on track: inside setContent(), we’ll find a ComposeView. Let’s explore what’s inside it to better understand how it works.

Kotlin
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

Let’s break down the body of the function:

Kotlin
val existingComposeView = window.decorView
    .findViewById<ViewGroup>(android.R.id.content)
    .getChildAt(0) as? ComposeView

Here, 

window.decorView:

  • The root view of the window where your activity’s content resides.

.findViewById<ViewGroup>(android.R.id.content):

  • android.R.id.content is the standard ID for the content view of an activity.
  • This line finds the view group that holds the content of the activity.

.getChildAt(0) as? ComposeView:

  • This retrieves the first child of the content view (assuming it’s a ComposeView).
  • The as? ComposeView safely casts the child to a ComposeView if it’s already present.

Next, there’s a check for whether the ComposeView already exists:

Kotlin
if (existingComposeView != null) with(existingComposeView) {
    setParentCompositionContext(parent)
    setContent(content)
}

if (existingComposeView != null):

  • Checks if a ComposeView is already present as the first child of the content view.

with(existingComposeView):

  • If the ComposeView exists, this block configures it:
  • setParentCompositionContext(parent): Sets the parent composition context for coordinating composition updates.
  • setContent(content): Sets the new composable content to be displayed in the ComposeView.

This approach reuses the existing ComposeView if available, avoiding the need to create a new one.

If no existing ComposeView is found, a new one is created:

Kotlin
else ComposeView(this).apply {
    // Set content and parent **before** setContentView
    // to have ComposeView create the composition on attach
    setParentCompositionContext(parent)
    setContent(content)
    // Set the view tree owners before setting the content view so that the inflation process
    // and attach listeners will see them already present
    setOwners()
    setContentView(this, DefaultActivityContentLayoutParams)   // Note : here this is composeview as it is inside apply 
}

ComposeView(this):

  • Creates a new ComposeView, passing the current ComponentActivity as the context.

setParentCompositionContext(parent):

  • Sets the parent composition context for coordinating updates.

setContent(content):

  • Sets the composable lambda as the content of the ComposeView.

setOwners():

  • Ensures the ViewTreeLifecycleOwner and ViewTreeViewModelStoreOwner are set. These owners are necessary for handling the lifecycle and ViewModel integration properly.

setContentView(this, DefaultActivityContentLayoutParams):

  • Sets the newly created ComposeView as the root view of the activity, using default layout parameters.

In short,

Reusing Existing ComposeView:

If there’s already a ComposeView, the function updates its content directly, improving efficiency by avoiding creating a new view.

Creating New ComposeView:

If no ComposeView exists, a new one is created and set as the activity’s content view.

Composition Context:

The parent parameter helps maintain the composable hierarchy and ensures updates are properly synchronized.

Lifecycle Awareness:

setOwners() ensures that the ComposeView has the necessary lifecycle owners before it gets attached to the activity.

By the way, what exactly is ComposeView?

I already gave a little hint, but there’s still more to explore. So, let’s dive into the details.

ComposeView is a special View provided by Jetpack Compose that serves as a bridge between the traditional Android View system and the Compose framework. Essentially, it allows you to embed composable UI within a standard Android View hierarchy.

Best Practices for Using setContent

Keep setContent Clean and Simple:

  • The lambda inside setContent should primarily call composable functions. Avoid complex logic inside the lambda to keep your code clean and readable.

Use Themes and Styling:

  • Wrap your content in a theme (e.g., MaterialTheme) to ensure consistent styling across your app.

Separate Concerns:

  • Structure your composables into separate functions and files based on their functionality. This improves readability and maintainability.

State Management:

  • Use remember and mutableStateOf for local state management within composables. For shared state, consider using ViewModel and LiveData or StateFlow.

Common Pitfalls to Avoid

Blocking the UI Thread:

  • Avoid long-running tasks or complex calculations inside setContent. Perform such tasks in a background thread using CoroutineScope.

Deeply Nested Composables:

  • Keep composable functions small and focused to avoid deeply nested structures, which can affect performance and readability.

Ignoring State Changes:

  • Ensure state changes trigger recomposition by using mutableStateOf or other state management solutions.

Conclusion 

As we wrap up, I hope you’ve gained a solid understanding of Composable Functions and how they simplify UI development. Jetpack Compose is a paradigm shift, but once you get the hang of it, you’ll realize its immense potential for creating beautiful, dynamic, and performant UIs.

Look, if you’re new to Jetpack Compose, start with simple composables and gradually explore more advanced concepts like state management, theming, and animations. 

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!