Google is committed to advancing racial equity for Black communities. See how.

MutatorMutex

@Stable class MutatorMutex
kotlin.Any
   ↳ androidx.compose.foundation.MutatorMutex

Mutual exclusion for UI state mutation over time.

mutate permits interruptible state mutation over time using a standard MutatePriority. A MutatorMutex enforces that only a single writer can be active at a time for a particular state resource. Instead of queueing callers that would acquire the lock like a traditional Mutex, new attempts to mutate the guarded state will either cancel the current mutator or if the current mutator has a higher priority, the new caller will throw CancellationException.

MutatorMutex should be used for implementing hoisted state objects that many mutators may want to manipulate over time such that those mutators can coordinate with one another. The MutatorMutex instance should be hidden as an implementation detail. For example:

import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope

@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)
        }

    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(block = block)
}

/**
 * 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")
        }
    }
}

Summary

Public constructors

Mutual exclusion for UI state mutation over time.

Public methods
suspend R
mutate(priority: MutatePriority = MutatePriority.Default, block: suspend () -> R)

Enforce that only a single caller may be active at a time.

suspend R
mutateWith(receiver: T, priority: MutatePriority = MutatePriority.Default, block: suspend T.() -> R)

Enforce that only a single caller may be active at a time.

Public constructors

<init>

MutatorMutex()

Mutual exclusion for UI state mutation over time.

mutate permits interruptible state mutation over time using a standard MutatePriority. A MutatorMutex enforces that only a single writer can be active at a time for a particular state resource. Instead of queueing callers that would acquire the lock like a traditional Mutex, new attempts to mutate the guarded state will either cancel the current mutator or if the current mutator has a higher priority, the new caller will throw CancellationException.

MutatorMutex should be used for implementing hoisted state objects that many mutators may want to manipulate over time such that those mutators can coordinate with one another. The MutatorMutex instance should be hidden as an implementation detail. For example:

import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope

@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)
        }

    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(block = block)
}

/**
 * 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")
        }
    }
}

Public methods

mutate

suspend fun <R> mutate(
    priority: MutatePriority = MutatePriority.Default,
    block: suspend () -> R
): R

Enforce that only a single caller may be active at a time.

If mutate is called while another call to mutate or mutateWith is in progress, their priority values are compared. If the new caller has a priority equal to or higher than the call in progress, the call in progress will be cancelled, throwing CancellationException and the new caller's block will be invoked. If the call in progress had a higher priority than the new caller, the new caller will throw CancellationException without invoking block.

Parameters
priority: MutatePriority = MutatePriority.Default the priority of this mutation; MutatePriority.Default by default. Higher priority mutations will interrupt lower priority mutations.
block: suspend () -> R mutation code to run mutually exclusive with any other call to mutate or mutateWith.

mutateWith

suspend fun <T, R> mutateWith(
    receiver: T,
    priority: MutatePriority = MutatePriority.Default,
    block: suspend T.() -> R
): R

Enforce that only a single caller may be active at a time.

If mutateWith is called while another call to mutate or mutateWith is in progress, their priority values are compared. If the new caller has a priority equal to or higher than the call in progress, the call in progress will be cancelled, throwing CancellationException and the new caller's block will be invoked. If the call in progress had a higher priority than the new caller, the new caller will throw CancellationException without invoking block.

This variant of mutate calls its block with a receiver, removing the need to create an additional capturing lambda to invoke it with a receiver object. This can be used to expose a mutable scope to the provided block while leaving the rest of the state object read-only. For example:

import androidx.compose.foundation.MutatorMutex
import androidx.compose.runtime.mutableStateOf

@Stable
class ScrollState(position: Int = 0) {
    private var _position = mutableStateOf(position)
    val position: Int by _position

    private val mutatorMutex = MutatorMutex()

    /**
     * Only [block] in a call to [scroll] may change the value of [position].
     */
    suspend fun <R> scroll(
        block: suspend MutableState<Int>.() -> R
    ): R = mutatorMutex.mutateWith(_position, block = block)
}
Parameters
receiver: T the receiver this that block will be called with
priority: MutatePriority = MutatePriority.Default the priority of this mutation; MutatePriority.Default by default. Higher priority mutations will interrupt lower priority mutations.
block: suspend T.() -> R mutation code to run mutually exclusive with any other call to mutate or mutateWith.