Jetpack Compose the declarative approach

Mubarak Native
6 min readMay 21, 2024

--

In this blog we are going to see what is declarative programming, and why industries are switching from imperative to declarative and how the compose builds the UI declaratively and lots more…

What is declarative and imperative programming ?

There are two ways to build UI’s, declarative and imperative, let’s first see imperative. This is a traditional way of building UI. In this approach, we manually update the state of the UI changes. When the data changes, we need to update the view by calling its setter like this view.setText this way of updating views manually leads to an illegal state and higher maintenance incase of declaratives. We don’t need to manually update the state.

How compose build UI declaratively with showing up-to-date content

To understand how compose show the up to date content without manually updating the state, we need to understand the core concept behind composable function that is Recomposition

Lifecycle of the composable

Recomposition is a process that happens on a composable function by calling the composable function again when the function input data changes, so it shows new current data. It also only calls the function or lambda that has changed and skips the rest. Many modern UI frameworks have already switched to declarative SwiftUI in IOS (Declarative), in the web development field React.js.

Creating a composable function

A function which is marked with @Composable is means, it is composable it convert data input into UI

@Composable
fun Greeting(@StringRes names: Int) { /* composable function */
Text(text = "Hello $name") /* Text Widget */
}

Compose Characteristics

Composable functions have a some sort of behaviour that we need to understand it before using it.

  • Composables can exectute in any order
  • Composable functions can run in parallel
  • A Composable might run frequently
  • Recompositon skip as much as possible
  • Recomposition is optimistic

Composable can exectute in any order

In a UI hierarchy of the composable it execute in any order based on recomposition, we can’t conclude the order of execute based on order its appears.

Composable functions can run in parallel

Recomposition is expensiv,e if its recompose a whole screen, So compose take a advantage of multicores to optimize it.

Composable might run frequently

In some situations, a composable function execute on each frame for cases such as animation. If we do expensive tasks such as reading values from device storage, triggering a network request, the function can cause UI janks lead to ANR (Application Not Responding) error.

To mitigate this problem execute that opearations in separate worker / background thread and observe the result data via observables such as mutableStateOf or LiveData.

Recompositon skip as much as possible

As i previously mentioned Compose recomposes a function whose data has been changed and skips the rest to avoid updating a whole UI tree.

Recomposition is optimistic

Recomposition is trigger whenever the parameter value changes, Compose expects to finish the recomposition before the parameters change again with new value becuase of not to miss any values. If a parameter does change before recomposition finishes, Compose might cancel the recomposition and restart it with the new parameter.

When to Recompose

As, i mention earlier in this blog compose recompose a composable function when a value change but we explicitly tell compose to recompose a this function when data changes let’s take some example

@Composable
fun Counter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count = 0
Text("Current countdown is $count") // current countdown value
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("+1")
}
}
}

This is a simple Counter composable function in this function, we have text and a button widget. we arrange this widget vertically by using Column layout when use see this function you may think when we Click the Button named +1 this will update the text by add +1 but this is not the case you think when we click this button the value won’t change anything not happen visually . This is becuase we doesn’t tell compose when to recompose this Counter function.

By using mutableStateOf function whose reads and writes are observed by the composable mutableStateOf is a generic function there are other State<T> function for primitive types such as: mutableIntStateOf, mutableLongStateOf, mutableFloatStateOf, or mutableDoubleStateOf

If we just use var count = mutableIntStateOf(0) we get a compile time warning, becuase this values this not persist over recomposition by using only this not useful nothing happens as before. You need to wrap it to remember {} to persist our value accross recomposition

Let’s update our code

@Composable
fun Counter(modifier: Modifier = Modifier) {
var count by remember { mutableStateOf(0) }

Column(modifier = modifier.padding(16.dp)) {
Text("Current countdown is $count") // current countdown value
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("+1")
}
}
}

Now that counter is working fine when clicking the button it will update our Text value, But this value is not persist on configuration change, such as: Orientation, Switching between light to dark, locale if you want to persist the simple values use rememberSavable {} in the place of remember.

For complex bussiness logic use ViewModel that is recommend way to store all our business logic it also survive configuration change .

State Hoisting

A composable function that manages its internal state by using remember that makes the composable stateful. if this function is that first in UI tree it is ok to manage themselves a state. However, composable with internal state is hard to reuseable and hard to test.

A composable function that doesn’t hold any state is know as stateless composable.

Composables that don’t hold any state are called stateless composables. An easy way to create a stateless composable is by using state hoisting.

State hoisting in Compose is a pattern of moving state to a composable’s caller to make a composable stateless. We achieve this by using kotlin function type see the example below.

@Composable
fun StatelessCounter(
modifier: Modifier = Modifier,
count: Int,
onIncrement: () -> Unit // function type
) {
Column(modifier = modifier.padding(16.dp)) {
Text("You've had $count glasses.")
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
  • onIncrement: () -> Unit — It is called when the button is clicked.

The above function is now stateless, the caller function is now define its state Ex:

@Composable
fun StatefulCounter() {
var counter1 by remember { mutableStateOf(0) }

var counter2 by remember { mutableStateOf(0) }

StatelessCounter(counter1, { counter1++ })
StatelessCounter(counter2, { counter2++ })
}

As you can see how we easily reuses this StatelessCounter by passing differenent implement. When we talk about reusablity I also want to introduce you with another pattern in compose, that help us lot to reuse our composables.

Slot Layout in compose

A pattern that help us to implement a new layer of customization on top of the existing composable. Slots leave an empty space in the UI for the developer to fill as they wish. The Material Component make heavy use of this pattern.

Composables usually take a content composable lambda ( content: @Composable () -> Unit). Slot APIs expose multiple content parameters for specific uses. Example

@Composable
fun HomeSection(
modifier: Modifier = Modifier,
title: String,
content: @Composable () -> Unit // slot layout
) {
Column(modifier) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = modifier
.paddingFromBaseline(top = 40.dp, bottom = 16.dp)
.padding(horizontal = 16.dp)
)
content()
}
}

I can reuse this Section to build my UI tree, such when implementing HomeScreen

Example:

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
Column(
modifier.verticalScroll(
rememberScrollState()
)
) {
Spacer(modifier = Modifier.height(16.dp))
SearchBar(Modifier.padding(horizontal = 16.dp))
HomeSection(title = "Top languages") { // reusing the same composable with different text
AlignYourBodyRow()
}
HomeSection(title = "Favourites") { // reusing the same composable
FavoriteCollectionsGrid(modifier)
}
Spacer(Modifier.height(16.dp))
}
}

As you can see, how it is easy to resue the composable by using this slot pattern.

For all the code samples, checkout the Compose-Code-Example Github repository.

This is all for this blog, I covered the basic of declarative programming incase of compose, there is a lot more in compose I planned to make it step by step in my other upcoming articles, I hope you will find this blog helpful, If it is don’t forgot to applause and I will see you in my next article.

Signing off 🫡, Mubarak Native

--

--

Mubarak Native

I am Mubarak "Native Android Developer" Focus on Clean and Minimal Code (simply love to write clean code)