Jetpack Compose Concepts Every Developer Should Know

Clinton Teegarden
CapTech Corner
Published in
8 min readMar 3, 2021

--

What is Compose?

Jetpack Compose is Android’s new declarative UI Framework. Android Developers have long been accustomed to writing UI in xml with Stateful Views that are updated by stepping through the View Hierarchy. With Jetpack Compose, UI is written in a Stateless manner through the use of Kotlin Functions.

Composable functions are annotated with the @Composable annotation. Composable functions must be annotated with this annotation which informs the compiler that this function adds UI to the View Hierarchy. While Composable functions can call other standard functions, Composables themselves can only be called from other Composables.

If you have not done so yet, I would highly encourage you to check out the learning path provided for Compose as it provides essential details and examples you can use to jump start your Compose Experience.

Unidirectional Data Flow

Compose is built off the standard of a Unidirectional Data Flow and it is expected that this paradigm is adhered to for proper implementation of Compose. Unlike the legacy Android UI system, Composables should be relatively Stateless — meaning their display state should be driven by arguments passed into the Composable function itself.

In its most basic sense, this means that events that either originate from the UI (button clicks, text entry, etc) or elsewhere (API Calls, Callbacks, etc) are processed by a handler that then in turn updates the UI State that is passed into Composable Functions. Since Composables are Stateless, the provided UI State will be used to build the UI.

In the flow above, the UI layer would be your Composable. Events originating from this layer, such as button clicks are passed to the Event Handler, such as a ViewModel. The ViewModel will provide the UI with State via LiveData/StateFlow. As the State is changed, updates are pushed to your Composables which are then recomposed using the newly updated State.

The code below demonstrates the above unidirectional flow — collecting state from the ViewModel, sending Events to the ViewModel, and the State being updated by the ViewModel.

MVI_ComposeTheme {
Surface(color = MaterialTheme.colors.background) {
val state = viewModel.viewState.collectAsState().value
Button(onClick = { viewModel.processEvent()}) {
Text(state.message)
}
}
}

Composition and Recomposition

Composition is the process in which your Composable functions are executed and the UI is created for the user. Recomposition is the process of updating the UI as a result of a State or Data Change that a Composable is using for its display. During recomposition, Compose is able to understand which data each Composable uses and only updates the UI components that have changed. The rest of the Composables are skipped.

Composition/Recomposition should not be equated to a LifeCycle.

  • Composable functions may be recomposed as often as every frame (i.e. animation)
  • Composable functions may be called in any order
  • Composable functions may be executed in parallel

This means that you should never include logic that executes when a Composable function is executed — sometimes referred to as Side-Effects.

Compose_Theme {
MainScreen()

// DO NOT DO THIS
viewModel.makeAPICall()
}

Stateful Composables & Good-Bye Saved Instance State!

While our goal is for our Composables to be largely Stateless, at times we need portions of them to be Stateful — for example to remember a scroll state, share a variable between Composables, etc. Since we know that recomposition can happen as often as every frame, it would not be ideal for the user to lose their scroll position every time recomposition would take place.

We can do this by creating and remembering a variable within our Composable:

val myInt = remember{ Random(10).nextInt() }

In the above example, the randomly generated integer will be remembered between compositions without recalculation. If this integer was not wrapped with remember, it would be recalculated on ever recomposition.

Taking this one step further, we can also remember this data between Configuration Changes too!

val myInt = rememberSaveable{ Random(10).nextInt() }

Sometimes we need to have a remembered variable that when updated by one Composable causes the recomposition of another Composable. In the example below, the button click increments the click counter, this counter is used within the Text Composable for display. Thus, when the Button is clicked, we want the Text to be displayed with the updated count.

To complete this, we will use the remember function provided by Compose as we did above, but the Integer will be wrapped with a MutableState object. The MutableState class is a single value holder whose reads and writes are observed by Compose and will cause recomposition of affected Composables.

Column{
// create state for buttonCount and a function to update it -
// setButtonCount
val (buttonCount, setButtonCount) =
rememberSaveable { mutableStateOf(0) }
Button(onClick = {
setButtonCount(buttonCount + 1)
}) {
Text(text = "Press Me!")
}
// recomposes whenever button is pressed
Text(text = "Button Pressed $buttonCount")
}

Slot APIs

Compose introduces the concept of Slot APIs. This allows Composables to be highly customizable without the Composable Functions providing infinite implementations for the various customizations you may apply. Since every use case and implementation may be different, Slot APIs provide empty slots within the composable where your customized UI can live.

For example, many Buttons provide more than just text inside of them. Some display loaders, some display icons on the left, some display icons on the right. Slots allow you to provide your own Composable that would provide these various customizations.

Slot outline for Button Composable

Other Composables, such as Scaffolds, are built entirely of Slots. Think of these Composables as an outline for what your UI would look like with reserved space for various UI components — such as Toolbar, BottomNav, Drawer, Screen Content, etc

Modifiers

Modifiers could be compared to xml Attributes that you traditionally use to style your UI, however, Modifiers are much easier to use and have a few more tricks. Modifiers allow you to decorate or change the default implementation of a Composable. You can change the appearance, add accessibility information, process UI event interactions, and more all through Modifiers. Modifiers are just Kotlin Objects so they can be added to for customized Modifiers as well.

Text(text = "Your Text", modifier = Modifier.padding(5.dp))

Modifiers are powerful as they provide an option to give your Composable layers without nesting it within other Composables. For example, the UI below would not be able to be achieved within Android’s Legacy UI system without nesting multiple Views.

However, with Modifiers in Compose we can achieve this with just one Composable. This is because the order that modifiers are applied matters and by leveraging padding and coloring in various orders we can achieve many different UI combinations.

Text(text = "Fake Button",
modifier = Modifier.padding(5.dp)
.background(Color.Magenta)
.padding(5.dp)
.background(Color.Yellow))

Lazy Lists

LazyLists is the Compose equivalent to RecyclerViews. Let me tell you that I will not miss the days of writing RecyclerView Adapters, ViewHolders, and all the other boilerplate code that goes along with them. The example below shows a LazyList in Column form (vertical scroll) that shows different UI elements based on the modulus of the integer. With a RecyclerView, this means we would need an Adapter and at least two different ViewHolders. With Compose, we just need our LazyColumn Composable with an items function that dynamically adds our content.

val listSize = 100
LazyColumn {
items(listSize) {
if (it % 2 == 0) {
Text("I am even")
}else{
Text("I am odd")
}
}
}

That is it. This might be my favorite thing to come out of Compose.

Constraint Layout

Compose has its own version of the ConstraintLayout that we have all come to know and love from the legacy UI system. ConstraintLayouts were pushed strongly in the legacy UI system as they provided a way for you to build highly customized UI with dependencies on other Views without a bunch of nested Views. With Compose, nesting Views is no longer the concern that it once was but at times we still need to leverage tools such as constraints, barriers, weights, etc that the ConstraintLayout has to offer.

ConstraintLayout {
// Creates references for the three Composables
val (button1, button2, text) = createRefs()
Button(
// constraintAs is like setting the ID, required.
modifier = Modifier.constrainAs(button1) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button 1")
}
// constraintAs is like setting the ID, required.
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button1.bottom, margin = 16.dp)
centerAround(button1.end)
})
// Create barrier to set the right button to the right of the
button or the text, which ever is longer.
val barrier = createEndBarrier(button1, text) Button(
modifier = Modifier.constrainAs(button2) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(barrier)
}
) {
Text("Button 2")
}
}

Compose & Navigation

Navigation within Jetpack Compose can leverage many of the features that we are accustomed to with Jetpack Navigation. However, with Jetpack Compose we now have the ability to Navigate between screens with a single Activity without the need to use Fragments.

Simply create a NavHost with your Screen Composable nested within them.

val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home"){
val vm: HomeVM = viewModel()
HomeScreen(vm)
}
composable("settings"){
val vm: SettingsVM = viewModel()
SettingsScreen(vm)
}
composable("profile"){
val vm: ProfileVM = viewModel()
ProfileScreen(vm)
}
}

With the use of androidx.lifecycle:lifecycle-viewmodel-compose you can create ViewModels within your Composables:

val vm: MyVM = viewModel()

ViewModels created within Composables will be retained until their scope (Activity/Fragment) are destroyed. This allows you to have ViewModels for your NavHost screens, much like you would do with Fragments today. In my example, I have the ViewModels created in the NavHost, this is just an example so that you can see their use.

To apply your NavHost to a BottomNav or other Navigation Component, leverage Scaffold:

Scaffold(
bottomBar = {
BottomNavigation {
// your navigation composable here
}
}
,
) {
NavHost(
navController = navController,
startDestination = "home") {
// your screen composables
}
}

Wrap Up

The above concepts are just an introduction to what Compose has to offer. Compose is a complete shift in the way that Android Developers have always built UI, but it is a welcomed change that greatly simplifies many of the challenges that the legacy UI system has. If you have not done so yet, I would highly encourage you to check out the learning path provided for Compose. It is a fairly long path, but every bit of it is worth the time. Happy Composing!

--

--

Clinton Teegarden
CapTech Corner

Mobile Lead & Architect @ CapTech. I specialize in delivering products for Fortune 500 clients in Mobile, Services and end to end solutions.