How To Understand Composables & Recomposition: A Beginners Guide To Jetpack Compose

The purpose of this article is to explain a few fundamental concepts and terms regarding the Jetpack Compose UI Library on Android. I intend to use the least amount of jargon necessary, and to connect new concepts with those which may be familiar to you when appropriate.

While this is a beginners guide to Compose, it will not be a beginners guide to Android, so readers will be expected to have built at least an application or two (though not in compose necessarily).

Before we begin, I was initially planning to write a follow up article directed towards more senior developers until I came across Leland Richardson’s two part article series. Leland is not only a Software Engineer working on the Jetpack Compose team, but I see that he is a great writer as well.

While I feel my article will stand on its own as an introduction to the basics of Jetpack Compose, I strongly suggest you read his articles once you have gained some practical experience with Compose; or right away if you prefer to learn that way.

A condensed version of this article in video format.

Key Terms/Concepts Explained in this article:

  • A brief review of the old View System and Hierarchy
  • Composables and how they stand in relation to Views
  • Recomposition and how to avoid doing it very poorly!
Musical Composer

What Is A Composable

In this section, we will discuss the most fundamental part of the Jetpack Compose library. If you are a seasoned Android developer, you may wish to skip to the sub-section titled “Are Composables Views?”

If you are not already familiar with the View system, you should read the next section as it is necessary to motivate and understand what a Composable is.

View Hierarchy

In the context of the Android SDK (the libraries we use to make user interfaces on this platform), a View is what we use to give structure and style to our applications.

It is the most fundamental kind of building block or element of a given user interface (UI), and each of these building blocks will contain the following kinds of information (among other things):

  • X and Y start and end positions which tell the computer where to draw the view on the device screen
  • Color and alpha (transparency) values
  • Font information, text, symbols, and images
  • Behaviour based on events such as user interaction (clicks) or changes in the application’s data (more on that later)

It is important to understand that a View can be something like a button (commonly referred to as a “widget”), but it can also be a container of the whole screen, part of the screen, or for other child Views.

Such containers (commonly referred to as Layouts or Viewgroups depending on the context), while sharing most of the same kinds of information as a widget, will also contain information about how to arrange and display other Views which are nested within them.

With that in mind, we get to the important part of this review of the View system: The View Hierarchy. For Web Developers, the View Hierarchy is essentially Android’s version of the Document Object Model (DOM).

For Android Developers, you can think of the View Hierarchy as a virtual representation of all the Views which you defined either in XML files or programmatically in Java or Kotlin.

To illustrate this, let us look at such an XML file (there’s no need to study it closely, just note the names), and then using a debugger/stepper tool, we will look at what it looks like in the memory space of the Fragment which inflates this file:

fragment_hour_view.xml:

<?xml version=”1.0" encoding=”utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=”http://schemas.android.com/apk/res/android"
android:layout_width=”match_parent”android:layout_height=”match_parent”android:id=”@+id/root_hour_view_fragment”xmlns:app=”http://schemas.android.com/apk/res-auto"><androidx.compose.ui.platform.ComposeViewandroid:id=”@+id/tlb_hour_view”//...
/>
<com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleViewandroid:id=”@+id/vqht_one”//.../><com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleViewandroid:id=”@+id/vqht_two”//.../><com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleViewandroid:id=”@+id/vqht_three”//.../><com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleViewandroid:id=”@+id/vqht_four”//.../></androidx.constraintlayout.widget.ConstraintLayout>

Memory Space of (Fragment)HourView.kt:

The debugger and stepper tools are some of my favourite ways to learn about what is going on under the hood of the code that I use from various libraries. Give it a try some time!

The purpose of showing you this XML file and what it turns into in a process (a process is simply a program that is running on a device), is to demonstrate how nested Views in an XML file translate into a nested View Hierarchy at runtime.

Hopefully, with a simple but concrete model of how the old system works, we can compare it with the new one.

Are Composables Views?

This was one of the first questions I asked when I started working with Compose, and the answer I have arrived at is both yes and no.

Yes, in the sense that a Composable fulfills the same conceptual role as a View in the old system. A Composable can be a widget like a button, or a container such as a ConstraintLayout (it is worth noting that there is a Composable implementation of ConstraintLayout available).

No, in the sense that the UI is no longer represented virtually in a View Hierarchy; apart from situations involving interoperability. With that being said, compose does not use magic to virtually represent and keep track of the UI, so it must have its own thing which is conceptually similar to a View Hierarchy.

Let us take a very brief look at this thing. Here, we have an Activity which uses the setContent {…} function to bind a Composable to itself:

ActiveGameActivity.kt:

class ActiveGameActivity : AppCompatActivity(), ActiveGameContainer {private lateinit var logic: ActiveGameLogicoverride fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    val viewModel = ActiveGameViewModel()    setContent {        ActiveGameScreen(            onEventHandler = {                logic.onEvent(it)            },            viewModel        )    }    logic = buildActiveGameLogic(this, viewModel, applicationContext)}//…}

ActiveGameScreen.kt:

@Composable
fun ActiveGameScreen(
onEventHandler: ((ActiveGameEvent) -> Unit),
viewModel: ActiveGameViewModel
) {
//...

GraphSudokuTheme {
Column(
Modifier
.background(MaterialTheme.colors.primary)
.fillMaxHeight()
) {
ActiveGameToolbar(
clickHandler = {
onEventHandler.invoke(
ActiveGameEvent.OnNewGameClicked
)
}
)

Box {
//content
}
}
}
}

In Compose, the View Hierarchy is replaced with something that we can locate if we dig really deeply into the mWindow field of this Activity. Within that field is the conceptual replacement of the View Hierarchy: The Composer and its slotTable.

At this point, if you want a detailed overview of the Composer and its slotTable, I must again suggest you read Leland’s article (he goes into detail in part 2). There is more to the Compose Hierarchy than the Composer and its slotTable, but that should be sufficient to get us started.

In general terms, Jetpack Compose uses what we might call its Compose Hierarchy (which is made of, and managed by things like the Composer and its slotTable).

Again, this is the same conceptual idea as the View hierarchy, a bunch of objects in memory space which collectively represent the UI, but it is implemented very differently.

An important difference, which is tricky to understand technically, but easy to understand in principle, is the way in which Compose handles updates to the Compose Hierarchy: Recomposition.

Recomposition: How To Update Compose UI

For my ESL friends, the word Compose comes from the latin componere, which roughly means “put together.” One who writes music is often called a “Composer,” which can be thought of as the one who puts together the notes coming from one or more instruments together into a composition (song).

Putting together implies that there are individual pieces, and it is important to understand that almost any good software developer makes at least some effort to break their code down into the smallest reasonable parts.

I mention reasonable, because I think principles like DRY (Don’t Repeat Yourself) should be followed only to the extent that they solve more problems than they create.

There are many benefits to applying this concept, which is often called modularity, or as I prefer, Separation of Concerns (SOC). I am aware that some of you reading this might think I am just copying what Leland said in his article, but I have been talking about SOC as the Golden Principle Of Software Architecture for many years already.

Where this plays into Compose, is the same principle which we see in the popular Javascript library React. When done properly, Compose will only “recompose” (redraw, rerender, update, whatever) the Composables (parts/elements of the UI) which need to be recomposed.

This is ENORMOUSLY important when it comes to the performance of an application, as redrawing the UI, whether in the old View system or in Compose, is costly for system resources.

In case you were not aware, the entire purpose of the old RecyclerView (which was the first thing I ever made a tutorial on back in 2016!) was to employ the ViewHolder pattern to a list of data, thus avoiding the need to constantly inflate (make) new Views for each list item.

My goal in this article was to focus mostly on the theory, as I will be writing plenty of practical content over the next few months. However, I will finish the article off with a story from my direct experience, which will help you to further understand how recomposition works, and how to avoid doing it very poorly!

The Stopwatch Example

For my first full Compose application, I decided to build Sudoku. There are a number of reasons why, including the fact that I wanted a project which did not have an insanely complicated UI, and I also wanted the chance to deep dive into Graph DS and Algos, which are quite suitable for Sudoku puzzles.

One thing I wanted, was a Stopwatch which kept track of how long it takes the user to complete a puzzle:

Screenshot of my Sudoku App, Graph Sudoku

As is often the case in my profession, I expected this timer to be much easier to add than it really was. I messed around with Android’s Chronometer class as well as the Java Timer class, and both of them presented different but application breaking problems.

Eventually I took a step back and realized that I was writing in Kotlin, and I set up a Coroutine based timer in my presentation logic class (it ended up making the most sense to put it there), which would update my viewmodel each second:

Class ActiveGameLogic(…):…{//…inline fun startCoroutineTimer(    delayMillis: Long = 0,    repeatMillis: Long = 1000,    crossinline action: () -> Unit) = launch {    delay(delayMillis)    if (repeatMillis > 0) {        while (true) {            action()            delay(repeatMillis)        }    } else {        action()    }}private fun onStart() =launch {    gameRepo.getCurrentGame(    { puzzle, isComplete ->        viewModel.initializeBoardState(            puzzle,            isComplete    )        if (!isComplete) timerTracker = startCoroutineTimer {            viewModel.updateTimerState()        }    },    {        container?.onNewGameClick()    }    )}//…}

The ViewModel (not from AAC, I write my own VMs, but Compose already as good interoperability with AAC VMs from what I can see) exposed references to callback functions, which is what I would use to update my Composables:

class ActiveGameViewModel {    //…    internal var subTimerState: ((Long) -> Unit)? = null    internal var timerState: Long = 0L    //…    internal fun updateTimerState(){        timerState++        subTimerState?.invoke(timerState)    }//…}

Now comes the important part! The way in which we can trigger recomposition of the Compose Hierarchy, is by using certain features of compose such as the remember function:

var timerState by remember {    mutableStateOf(“”)}

If you must know, these features store the state of whatever you are remembering in the slotTable. In short, the word state here means the current “state” of the data, which starts out being just an empty String.

Here is where I screwed things up. I had pulled my simple timer composable into its own function (applied SOC), and I was passing in timerState as a parameter to that composable.

However, the above snippets were sitting in the parent composable of the timer, which was a container for the most complicated portion of the UI (a 9x9 Sudoku requires a large number of widgets):

@Composable
fun GameContent(
onEventHandler: (ActiveGameEvent) -> Unit,
viewModel: ActiveGameViewModel
) {
Surface(
Modifier
.wrapContentHeight()
.fillMaxWidth()
) {
BoxWithConstraints(Modifier.background(MaterialTheme.colors.primary)) {
//…
ConstraintLayout {
val (board, timer, diff, inputs) = createRefs()
var isComplete by remember {
mutableStateOf(false)
}
var timerState by remember {
mutableStateOf("")
}
viewModel.subTimerState = {
timerState = it.toTime()
}
viewModel.subIsCompleteState = { isComplete = it }
//…Sudoku board
//Timer
Box(Modifier
.wrapContentSize()
.constrainAs(timer) {
top.linkTo(board.bottom)
start.linkTo(parent.start)
}
.padding(start = 16.dp))
{
TimerText(timerState)
}
//…difficulty display
//…Input buttons
}
}
}
}
@Composable
fun TimerText(timerState: String) {
Text(
text = timerState,
style = activeGameSubtitle.copy(color = MaterialTheme.colors.secondary)
)
}

This was causing some considerable lag and unresponsiveness, and by making heavy usage of the debugger, I was able to find out why: Because my timerState variable was created and updated inside the parent Composable, it was triggering a recomposition of that entire portion of the UI. Every. Single. Tick.

After moving the appropriate code into the TimerText composable, things worked very smoothly:

@Composable
fun TimerText(viewModel: ActiveGameViewModel) {
var timerState by remember {
mutableStateOf("")
}

viewModel.subTimerState = {
timerState = it.toTime()
}

Text(
text = timerState,
style = activeGameSubtitle.copy(color = MaterialTheme.colors.secondary)
)
}

Hopefully I have given you a working understanding of recomposition and one of the biggest ways to do it wrong. Avoiding unnecessary recompositions is incredibly important for performance, and so far it seems that applying SOC rigorously, even to the point of keeping remember state in separate composables, should become standard practice.

Resources & Support

If you liked this article, hit the clap and consider following me on medium and my other platforms. I have a youtube channel with hundreds of tutorials, and am an active writer on various platforms.

Social

https://www.instagram.com/rkay301/
https://www.facebook.com/wiseassblog
https://twitter.com/wiseAss301

Tutorials & Courses

https://youtube.com/wiseass https://www.freecodecamp.org/news/author/ryan-michael-kay/ https://skl.sh/35IdKsj (introduction to Android with Android Studio)

Also, I want to point out the single resource I used to get started with Jetpack Compose: Working code samples from good developers.

Self-taught software developer & student of computer science.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store