Getting Started with Jetpack Compose (dev15)

Halil Ozercan
7 min readJul 29, 2020

This write-up is divided into 5 cohesive parts for better delivery. This post is about getting into Jetpack Compose for the first time and figuring out little details before moving onto more concrete implementations with clear goals. The link for next posts can be found at the bottom.

What is Jetpack Compose?

Jetpack Compose is a modern toolkit for building native Android UI. It’s based on the declarative programming model, so you can simply describe what your UI should look like, and Compose takes care of the rest — as app state changes, your UI automatically updates.

Declarative UI frameworks took the web by storm with the introduction of React from Facebook. Virtual DOM changed the way developers think about how UI should be designed and controlled by code. Imperative methods served good over decades but they got exponentially complicated with the increasing complexity of modern products. Moreover, front-end teams started to separate with respect to their responsibilities of features. This kind of modularization required the architecture of written software to reflect the same change according to Conway’s Law. Declarative frameworks like React and Vue addressed these and other problems about modern UI. Later, they were followed by Flutter and SwiftUI both of which proved that the same paradigm can be realized on mobile phones.

Android SDK was first published in late 2000s when UI programming was still living the old days. An XML-like mark-up to define the layout which then can be modified by Controller classes like Activities and Fragments. However, their simplicity no longer holds since many UI elements are now reactive by design, observing ever changing data sources and reflecting accordingly. Writing observer callbacks for LiveData objects gets cumbersome after a while. To address these issues, Android Tools Team has undertaken a “revolutionary” project called Jetpack Compose to address the needs of modern UI for the upcoming decade. Native Android views will be inflated by Kotlin code through Composition, rather than XML definitions. To learn more about Compose, please visit Google’s own documentation site.

Setting Up

We are here to learn more about Compose, so let’s get started. First thing to note is that Jetpack Compose is still under heavy development. Current releases are published under “Developer Preview” tag which means that the APIs are subject to change(yes, they heavily do). Hence, Jetpack Compose is available only through Android Studio Canary channel. You’re going to have to download Android Studio 4.2 Canary release by the time of writing this article. It is subject to change because when 4.1 hits the main release channel, Compose will likely migrate to 4.3 because it will be on Canary channel.

Download Android Studio Canary

Once you’re done with download and initial setup, let’s start a new project with Compose support by following

> Create new Project > Empty Compose Activity

Select Empty Compose Activity to enable Compose support

In the next screen, enter whatever you like for application name and other properties which should not matter for the rest of the series. Unfortunately, Android Studio still starts a new Compose project using 0.1.0-dev13 version which is a little bit old right now. We’re going to upgrade it to dev15 but it requires some tweaking.

In the root build.gradle file, make sure the bold parts are up-to-date.

buildscript {
ext {
compose_version = '0.1.0-dev15'

}
ext.kotlin_version = "1.4-M3"

repositories {
google()
jcenter()
maven { url = "https://dl.bintray.com/kotlin/kotlin-eap" }
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.0-alpha05"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
maven { url = "https://dl.bintray.com/kotlin/kotlin-eap" }
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

Go to build.gradlefile under app directory, change kotlin compiler version for Compose.

composeOptions {
kotlinCompilerExtensionVersion "${compose_version}"
kotlinCompilerVersion "1.4.0-dev-withExperimentalGoogleExtensions-20200720"
}

and add the following to the end

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += ["-Xallow-jvm-ir-dependencies", "-Xskip-prerelease-check"]
}
}

I wish we were ready now but Compose made quite a bit more change in dev15, most importantly the naming of Compose packages. You can find the complete list of these changes here. Again go to build.gradle file and replace old dependencies with new ones.

These two changes were enough on my setup

implementation "androidx.compose.foundation:foundation-layout:$compose_version"
implementation "androidx.compose.material:material:$compose_version"

Finally, run the application and you should see Hello Android! on the screen.

State, React, Compose

Let’s check out this initial code.

setContent {
ComposeTubeTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}

Here, we can immediately see that setContentView(R.layout.activity_main) is no longer. Instead, there is a setContent extension function on Activity that takes in a @Composable lambda. This is our little rabbit hole to get into the world of Compose. From here on, under this scope, every composable function will have a chance to affect the UI.

We start with a Surface, which takes a background argument. The trailing lambda is also another Composable which will be rendered on the given Surface. This is the most basic relation that can be found in any declarative UI framework. The UI tree is now being constructed through use of content composables.

Greeting is a custom Composable that takes in an argument called name and shows a Text with the said argument. When we checkout our Tree, we have

Surface > Greeting -> Text

However, this page is currently completely static. There is no user interaction or outsource data that will affect how the UI looks. Let’s change that by introducing a button that should add another Hello Android! text.

setContent {
ComposeTubeTheme {

val (count, setCount) = state { 1 }

Surface(color = MaterialTheme.colors.background) {
Column {
Button(onClick = {
setCount(count+1)
}) {
Text("Add")
}
(1..count).forEach {
Greeting("Android")
}
}
}
}
}

Wait a minute. What is state function? State is one of the building blocks of Compose. state function returns an instance of State class which has a special place in Compose compiler. Any time a state changes, its scoped Compose gets invalidated and recomposes itself. In this case, we increase the count when we click on a button. This triggers a recomposition of the whole view. I cannot go into the details of state and recomposition works in this arcitle, so you will have to read a little bit of documentation for that. While reading the documentation, I also strongly suggest looking at another foundational function and paradigm called remember .

https://developer.android.com/reference/kotlin/androidx/compose/package-summary#state

Composition Callbacks

Managing the state is not an easy task. It requires many trials and errors, sometimes logging to truly understand what is going around, what is being triggered according to which changes. Compose gives us strong tools that are capable of manipulating the every pixel on the screen. Developers should be vary of manipulating state carelessly because each recomposition has a cost despite the fact that it is optimized heavily.

For that reason, main composition callbacks are worth practicing over because they let developers hook into composition lifecycle whenever necessary. Main callbacks are also listed in the documentation I linked

onActive

  • The callback will execute once initially after the first composition is applied, and then will not fire again.
  • onActive is called once the View is added to the Composition tree.

onPreCommit

  • The onPreCommit effect is a lifecycle effect that will execute callback every time the composition commits, but before those changes have been reflected on the screen

onCommit

  • The onCommit effect is a lifecycle effect that will execute callback every time the inputs to the effect have changed.
  • It is the best place to have any side-effects related to the changing inputs.

launchInComposition

  • Launch a suspending side effect when this composition is committed and cancel it when launchInComposition leaves the composition.

onPreCommit, onCommit, and launchInComposition have variants which take arbitrary variables as arguments. These variants are used as a controlling unit to decide whether to invoke these callbacks. If variables haven’t been changed since the last time the callback was called, then callback does not get triggered. The inverse is also true.

onDispose

  • An effect used to schedule work to be done when the effect leaves the composition.

A little experiment

It is not an easy to task to completely understand when these callbacks are triggered or what are they useful for. They all exist for some use case. To better understand their nature, I prepared this simple app.

After hitting the increase button two times, resulting logs are:

onCompose remembered Counter: 0
launched in composition
onActive Counter: 0
onCommit Counter: 0
ShowTextAmbient commits
ShowTextAmbient2 commits
ShowTextAmbient3 commits
ShowTextRegular commits
ShowTextRegular2 commits
ShowTextRegular3 commits
launched in composition after 2 seconds
onCompose remembered Counter: 0
onCommit Counter: 1
ShowTextAmbient3 commits
ShowTextRegular commits
ShowTextRegular2 commits
ShowTextRegular3 commits
onCompose remembered Counter: 0
onCommit Counter: 2
ShowTextAmbient3 commits
ShowTextRegular commits
ShowTextRegular2 commits
ShowTextRegular3 commits
  • Remember always returns the same result from when it was first invoked.
  • onActive is called only once when the view is first composed
  • onCommit is triggered after every state change.
  • Ambients are convenient when you want to pass down items in a nested structure. They eliminate unnecessary composition in between.

Next

--

--