Jetpack Compose: Update state from pointer input

Jetpack Compose is on the way to take the UI development world by storm. Not only will it be the de facto default for Android, but support for Desktop/Web/Shell/iOS are all well underway. With so much prospect, it is a good time to dig into it.

Creating a compelling UI and connecting it to data in a robust way is an easy task. However, some aspects require insights due to the composable nature of the UI and its interaction with background work in a coroutine. In this particular instance, the interaction with a touch event on a composable demonstrates the culprit.

Implementing gesture detection

The following snippet shows a PositionSelector composable, which allows dragging up and down on a text label. Dragging up and down respectively increases and decreases the value. It looks straightforward but does not work as expected. The position is only increased by one drag callback.

State not updating as expected

The issue we are facing resides in the Modifider.pointerInput(..). In the detectVerticalDragGesture(..) we calculate the new position value from the dragAmount and the previous position. With the newly created position we modify the state of the position var outside of the composable. This in turn recomposes the PositionSelector and updates the text label.

The problem stems from the recomposition not passing the updated position value to the pointerInput(..). Instead, the one initially passed in position is used on the second drag event as well. This results in a new position value, which is always recalculated based on the initial value and the current drag offset.

Discovering a solution

The docs of the pointerInput(..) lead us onto the correct path by stating:

The pointer input handling block will be canceled and re-started when pointerInput is recomposed with a different key1.

This would imply that we should change the key from Unit to the newly passed position value. Unfortunately, this would cancel and recreate the listening infrastructure, which is not what we want. To get more insights into what is going on, we have to inspect the sources of pointerInput(..). In the code, we find that a LaunchedEffect is used to run our callback block.

With this insight, we have to recall the behavior of the LaunchedEffect. Its core feature is to run its effect when entering the composition. This is a one-time operation until key changes. Therefore our initial position value is always used in the callback since it is captured during the initial composition.

In order to always use the most recent position value in our code block, we have to inform compose to do so. The mechanism for this is to warp the position via rememberUpdatedState(position). Applying it in the snippet above, we get:

Wrapping the position in the currentPosition allows the internal LaunchedEffect to always receive the latest value.

Wrap Up

Jetpack Compose is a powerful new UI framework with tons of potential. Using it to create a UI is very simple, but to make the most out of it requires some knowledge of the internals. To get a broader view on the topic, it is advised to read the docs on the side effects api. Hopefully, the Compose team will improve its docs in the future. At this point, it is required to read the sources occasionally.

2 replies
  1. HRJ
    HRJ says:

    Thank you, this helped a lot! I was wracking by brains for 3 hours before realizing that the offset is not being updated.

    Reply
  2. Daniil Shevtsov
    Daniil Shevtsov says:

    Thank you, your article just saved my sanity. I couldn’t understand why my code breaks when I simply extract a method. It seems I need to learn more about state managment in Compose.

    Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *