Why you can’t use random Coroutine scope in Compose?

Rajesh Hadiya
3 min readAug 17, 2024

--

Let’s say we are building a LazyList with a button at the bottom, which scrolls to the bottom of the list with a smooth animation.

We can build it using the code below.

Here, we’re using a ready-made function provided by compose to create a Coroutine scope, tied to this composable function. This scope will be used to launch animated scroll to bottom.

val scope = rememberCoroutineScope()
scope.launch { listState.animateScrollToItem(list.lastIndex) }

What if we want to create our own Coroutine scope? As this is an UI operation, we’ use Dispatchers.Main which represents Android’s main thread.

val customScope = CoroutineScope(Dispatchers.Main)
customScope.launch { listState.animateScrollToItem(list.lastIndex) }

If you run the app, and try to click “Scroll to Bottom” button, the app will crash with the following exception.

java.lang.IllegalStateException: A MonotonicFrameClock is not available in this CoroutineContext. Callers should supply an appropriate MonotonicFrameClock using withContext.

What is MonotonicFrameClock?

It is a way to keep track of time in a consistent and predictable manner during animations or other time-based operations.

  • Monotonic: This means that the time it provides only moves forward and never goes backward. This ensures that time is always increasing, which is important for smooth animations.
  • FrameClock: This is a special clock that gives you the current time for each frame of an animation. Every time a new frame needs to be drawn (which happens many times per second), the clock gives you a new timestamp.

Why our own CoroutineScope is missing it? and why it’s available to the composable function that gives us a coroutine scope?

To understand this, we need to understand CoroutineContext and Element.

CoroutineContext

The CoroutineContext is a collection of elements that define various aspects of a coroutine's behavior. It is like a "configuration" for coroutines. Some examples are: Job, Dispatcher, CoroutineExceptionHandler.

Element

An Element is a single component of the CoroutineContext. Each Element is a key-value pair where the key is a CoroutineContext.Key, and the value is the Element itself. Elements in the context can be retrieved or modified based on their key.

How does all of this work together?

  1. When you use rememberCoroutineScope(), compose creates a new scope and attach the MonotonicFrameClock to the coroutine context as element.
  2. Since CoroutineContext represents a collection, we can find an element with a concrete key using get.
  3. When you run any suspend function for animation (or whatever that needs this clock), compose tries to get the reference to this clock.
  4. If it founds the clock, it’ll make use of it, otherwise, it’ll throw an exception as mentioned above.

How to make it work with our custom scope?

  1. To make it work, you either create your own instance of MonotonicFrameClock or use a dispatcher that provides it.
  2. AndroidUiDispatcher class from androidx.compose.ui.platfomr package does provide this clock, which we can use in our scope.
  3. Then we can modify the code as below.
val customScope = CoroutineScope(AndroidUiDispatcher.Main)
customScope.launch { listState.animateScrollToItem(list.lastIndex) }

This will work perfectly fine, as it has access to MonotonicFrameClock .

Note: You should use scope provided by compose and avoid using a custom scope, unless required.

If you enjoyed the article, follow me to learn more interesting topics on Android. You can also follow me on LinkedIn where I write interesting stuff daily.

Thanks for reading ❤️

--

--

Rajesh Hadiya

Founder of MyStore | Talks about Kotlin, Android and Spring Boot