How To Handle UI Events In Jetpack Compose

In this short and practicle article, we will talk about handling UI events, such as onClick events, by taking full advantage of the Kotlin’s Function Types and Sealed Classes.

Here’s the short version.

If you have not already learned about what a composable is, consider reading this article which explains the fundamentals. In any case, the important thing to understand is that composables are functions, not classes!

This means that we have to take a different approach than typical OOP solutions, but I will keep things simple to learn and apply.

Cat interacting with phone.

How To Model UI Events With A Sealed Class

First, we must understand what is meant by UI Events and how to use a language feature which is perfect for modelling them: Sealed Classes.

I have described this same process for Java and Kotlin (with the old view system) before, so I will keep this brief.

The Process

For each part of the UI (assume for now we are talking about one single screen of an App, but you can adjust things accordingly), ask yourself this question: What are all the different ways which the user can interact with this it?

Let us take an example from my first app built fully in compose, Graph Sudoku:

Graph Sudoku screenshot in 4x4 puzzle mode

The sealed class I use to represent the UI interactions of this screen looks like this:

sealed class ActiveGameEvent {
data class OnInput(val input: Int) : ActiveGameEvent()
data class OnTileFocused(val x: Int,
val y: Int) : ActiveGameEvent()
object OnNewGameClicked : ActiveGameEvent()
object OnStart : ActiveGameEvent()
object OnStop : ActiveGameEvent()
}

To explain briefly:

  • OnInput represents when a user hits an input button (i.e. 0, 1, 2, 3, 4)
  • OnTileFocused represents a user selecting a tile (like the amber highlighted one)
  • OnNewGameClicked is self-explanatory
  • OnStart and OnStop are actually lifecycle events which my composables do not care about, but they are used in the Activity which acts as a Container for the composables

Once you have your sealed class set up, you can now handle a wide variety of different events using a single event handler function. Sometimes it might make more sense to have multiple event handler functions, so keep in mind that this approach must be adapted to project specific requirements.

How To Connect Your Software Architecture

What you have handling these events is totally up to you. Some people still think that MVVM is the golden standard of software architectures, but it seems like more and more people are realizing that there is no single architecture which works best for every situation.

For Android with compose, my current approach is to use a very 3rd party minimalist (no LiveData or Jetpack ViewModel dependencies) approach which typically has these things in each feature (screen):

  • A (Presentation) Logic class as an event handler
  • A ViewModel to do exactly what the name implies (unlike how many people use them)
  • An Activity which acts as a Container (not a god object!)
  • Composables to form the View
Model-View-Whatever dude.

I do not care what you use as long as you are applying separation of concerns. In fact, this is how I arrived at this architecture, by simply asking what should and should not be put together in the same class.

Anyways, whether you want your ViewModel, a Fragment, or an Activity to be your event handler, all of them can be set up the same way: Function Types!

Within your class of choice, set up an event handler function which accepts your sealed class as its argument:

class ActiveGameLogic(
private val container: ActiveGameContainer?,
private val viewModel: ActiveGameViewModel,
private val gameRepo: IGameRepository,
private val statsRepo: IStatisticsRepository,
dispatcher: DispatcherProvider
) : BaseLogic<ActiveGameEvent>(dispatcher),
CoroutineScope {
//...
override fun onEvent(event: ActiveGameEvent) {
when (event) {
is ActiveGameEvent.OnInput -> onInput(
event.input,
viewModel.timerState
)
ActiveGameEvent.OnNewGameClicked -> onNewGameClicked()
ActiveGameEvent.OnStart -> onStart()
ActiveGameEvent.OnStop -> onStop()
is ActiveGameEvent.OnTileFocused -> onTileFocused(event.x, event.y)
}
}
//...
}

This approach is very organized and it makes it so that you can test every unit in this 3rd party library free (thus easier to test) class through a single gateway.

However, we are not done yet! Naturally, we need a way to get a reference to this event handler function, onEvent, to our Composables. We can do this using a function reference:

class ActiveGameActivity : AppCompatActivity(), ActiveGameContainer {
private lateinit var logic: ActiveGameLogic

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val viewModel = ActiveGameViewModel()

setContent {
ActiveGameScreen(
onEventHandler = logic::onEvent,
viewModel
)
}

logic
= buildActiveGameLogic(this, viewModel, applicationContext)
}

override fun onStart() {
super.onStart()
logic.onEvent(ActiveGameEvent.OnStart)
}

override fun onStop() {
super.onStop()
logic.onEvent(ActiveGameEvent.OnStop)
//...
}

override fun onNewGameClick() {
//...
}

override fun showError() = makeToast(getString(R.string.generic_error))
}

I am sure some of you are wondering why I am using an Activity. You can ask me during a livestream Q&A sometime for a detailed answer.

In short, Fragments appear to be a bit pointless with Compose (big fan of them in the old View system though!), and there is nothing wrong with using Activities as a feature specific container. Just avoid writing god activities basically.

Just to be specific, the way you make a reference to a function in Kotlin, is by providing the class/interface name (or skip that if it is a Top-Level function), followed by two colons, and the name of the function without any arguments or brackets:

onEventHandler = logic::onEvent

To How Handle onClick Events Within In Jetpack Compose

With that stuff ready, we can now look at how this works within the composable. Naturally, your root composable will need to list the event handler function as a parameter:

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

It can be a bit tricky to get function type syntax correctly, but understand that this really is a reference to a function; which is not so different from a reference to a class.

Now, just as you should not build god objects, you should not build giant composables:

  1. Break your UI down into the smallest reasonable parts
  2. Wrap them in a composable function
  3. For each of those composables which has a UI interaction associated with it, it must be given a reference to your event handler function

Here is a composable which represents the input buttons of the Sudoku app, which is given the event handler by reference:

@Composable
fun SudokuInputButton(
onEventHandler: (ActiveGameEvent) -> Unit,
number: Int
) {
Button(
onClick = { onEventHandler.invoke(ActiveGameEvent.OnInput(number)) },
modifier = Modifier
.requiredSize(56.dp)
.padding(2.dp)
) {
Text(
text = number.toString(),
style = inputButton.copy(color = MaterialTheme.colors.onPrimary),
modifier = Modifier.fillMaxSize()
)
}
}

To actually pass the event to the logic class, we must use the invoke function, which will accept arguments as per the function type definition (which accepts an ActiveGameEvent in this case).

At this point, you are ready to handle UI interaction events in Kotlin (compose or not) by taking full advantage of this beautiful and modern programming language.

If you liked this article, hit the clap button and consider checking out the resources below to support an independent programmer and content creator.

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)

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