Why you can’t use random Coroutine scope in Compose?
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?
- When you use
rememberCoroutineScope()
, compose creates a new scope and attach theMonotonicFrameClock
to the coroutine context as element. - Since
CoroutineContext
represents a collection, we can find an element with a concrete key usingget
. - When you run any suspend function for animation (or whatever that needs this clock), compose tries to get the reference to this clock.
- 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?
- To make it work, you either create your own instance of
MonotonicFrameClock
or use a dispatcher that provides it. AndroidUiDispatcher
class fromandroidx.compose.ui.platfomr
package does provide this clock, which we can use in our scope.- 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 ❤️