Mutual exclusion for UI state mutation over time.
mutatorMutexStateObject
fun mutatorMutexStateObject() {
@Stable
class ScrollState(position: Int = 0) {
private var _position by mutableStateOf(position)
var position: Int
get() = _position.coerceAtMost(range)
set(value) {
_position = value.coerceIn(0, range)
}
private var _range by mutableStateOf(0)
var range: Int
get() = _range
set(value) {
_range = value.coerceAtLeast(0)
}
var isScrolling by mutableStateOf(false)
private set
private val mutatorMutex = MutatorMutex()
/** Only one caller to [scroll] can be in progress at a time. */
suspend fun <R> scroll(block: suspend () -> R): R =
mutatorMutex.mutate {
isScrolling = true
try {
block()
} finally {
// MutatorMutex.mutate ensures mutual exclusion between blocks.
// By setting back to false in the finally block inside mutate, we ensure that
// we
// reset the state upon cancellation before the next block starts to run (if
// any).
isScrolling = false
}
}
}
/** Arbitrary animations can be defined as extensions using only public API */
suspend fun ScrollState.animateTo(target: Int) {
scroll { animate(from = position, to = target) { newPosition -> position = newPosition } }
}
/**
* Presents two buttons for animating a scroll to the beginning or end of content. Pressing one
* will cancel any current animation in progress.
*/
@Composable
fun ScrollControls(scrollState: ScrollState) {
Row {
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { scrollState.animateTo(0) } }) {
Text("Scroll to beginning")
}
Button(onClick = { scope.launch { scrollState.animateTo(scrollState.range) } }) {
Text("Scroll to end")
}
}
}
}