Package
Xamarin.AndroidX.Compose.Foundation / Xamarin.AndroidX.Compose.Foundation.Android
Version
1.11.1 (also reproduces on 1.10.4.1)
Summary
androidx.compose.foundation.MutationInterruptedException is present in the upstream JAR (androidx/compose/foundation/MutationInterruptedException.class) but is silently dropped during binding. The class is not removed by source/androidx.compose.foundation/foundation-android/Transforms/Metadata.xml, yet zero references to it appear in the consumer-built Xamarin.AndroidX.Compose.Foundation.Android.dll.
Sibling types in the same MutatorMutex.kt file bind correctly:
| Symbol |
Bound? |
androidx.compose.foundation.MutatorMutex |
✅ |
androidx.compose.foundation.MutatePriority |
✅ |
androidx.compose.foundation.internal.PlatformOptimizedCancellationException (the parent) |
✅ |
androidx.compose.foundation.MutationInterruptedException |
❌ |
Why it matters
MutationInterruptedException is what Compose throws into await-able state-mutation APIs (e.g. ScrollState.animateScrollTo, ScrollState.scrollTo, LazyListState.animateScrollToItem, AnchoredDraggableState.anchoredDrag, all Scrollable*.scroll(...) paths) when a higher-priority caller wins the MutatorMutex mid-flight — most commonly when the user touches the scroll surface during a programmatic animated scroll, which ejects the running animation.
Without the binding, C# callers using kotlinx.coroutines interop have no typed exception to catch and must fall back to catch (Java.Lang.Throwable), which over-catches and is what most Android-style code-review guidance discourages. With the binding, they could write the natural:
try
{
await scrollState.AnimateScrollToAsync(targetValue, ct);
}
catch (MutationInterruptedException)
{
// gesture or higher-priority mutation won; ignore
}
This is the same gap referenced in #1456 (the C# scroll-suspend reproduction) and #1440 (binder mangled-name handling) -- this one is more narrowly about a single missing public type.
Reproduction
Disassemble the bound DLL after a consumer build:
$dll = "src/<YourApp>/obj/Debug/net10.0-android/android/assets/arm64-v8a/Xamarin.AndroidX.Compose.Foundation.Android.dll"
$bytes = [IO.File]::ReadAllBytes($dll)
foreach ($needle in "MutationInterruptedException","MutatorMutex","PlatformOptimizedCancellation") {
$pattern = [Text.Encoding]::ASCII.GetBytes($needle)
$count = 0
for ($i = 0; $i -lt $bytes.Length - $pattern.Length; $i++) {
$m = $true
for ($j = 0; $j -lt $pattern.Length; $j++) { if ($bytes[$i+$j] -ne $pattern[$j]) { $m = $false; break } }
if ($m) { $count++; $i += $pattern.Length }
}
"$needle : $count"
}
Output:
MutationInterruptedException : 0
MutatorMutex : 6
PlatformOptimizedCancellation : 2
Confirm the class IS in the upstream JAR by extracting androidx.compose.foundation.foundation-android.aar -> classes.jar and running:
javap -p -s androidx/compose/foundation/MutationInterruptedException.class
Compiled from "MutatorMutex.kt"
public final class androidx.compose.foundation.MutationInterruptedException
extends androidx.compose.foundation.internal.PlatformOptimizedCancellationException {
public static final int $stable;
public androidx.compose.foundation.MutationInterruptedException();
static {};
}
The class is public final, has a public no-arg ctor, and extends a Kotlin abstract class that has both a public (String) ctor and the synthetic (String, int, DefaultConstructorMarker) no-arg helper.
Suspected cause
The parent PlatformOptimizedCancellationException declares its no-arg ctor via the Kotlin synthetic-default pattern ((String s, int default, DefaultConstructorMarker marker) + a public X() synthetic). The binder typically surfaces only the explicit (String) overload on the parent, so the subclass loses its only chainable base ctor and gets silently dropped by a later validation pass.
(Sibling types like MutatorMutex and MutatePriority bind fine because their parents have unambiguous accessible base ctors.)
Proposed fix
If the root cause is the dropped no-arg base ctor, adding a <add-node> in source/androidx.compose.foundation/foundation-android/Transforms/Metadata.xml may reintroduce the class. Rough shape:
<add-node path="/api/package[@name='androidx.compose.foundation']">
<class abstract="false" deprecated="not deprecated" final="true"
name="MutationInterruptedException" static="false" visibility="public"
extends="androidx.compose.foundation.internal.PlatformOptimizedCancellationException"
extends-generic-aware="androidx.compose.foundation.internal.PlatformOptimizedCancellationException"
jni-extends="Landroidx/compose/foundation/internal/PlatformOptimizedCancellationException;"
jni-signature="Landroidx/compose/foundation/MutationInterruptedException;">
<constructor deprecated="not deprecated" final="false"
name="MutationInterruptedException" static="false"
visibility="public" bridge="false" synthetic="false" />
</class>
</add-node>
Alternatively, restoring the no-arg ctor on the parent (so the subclass binds naturally) would be cleaner if the binder allows surfacing the synthetic.
Workaround
C# callers can still catch the exception, just not by type:
try
{
await scrollState.AnimateScrollToAsync(targetValue, ct);
}
catch (Java.Lang.Throwable t) when (t.Class.Name == "androidx.compose.foundation.MutationInterruptedException")
{
// gesture won the mutex
}
This works but is fragile, ugly, and discoverability is poor.
Environment
- .NET 10 /
net10.0-android36.0
Xamarin.AndroidX.Compose.Foundation 1.11.1
Xamarin.AndroidX.Compose.Foundation.Android 1.11.1
- Discovered while building jonathanpeppers/compose-net, specifically the
SuspendBridge -> ScrollState.AnimateScrollToAsync flow.
Package
Xamarin.AndroidX.Compose.Foundation/Xamarin.AndroidX.Compose.Foundation.AndroidVersion
1.11.1 (also reproduces on 1.10.4.1)
Summary
androidx.compose.foundation.MutationInterruptedExceptionis present in the upstream JAR (androidx/compose/foundation/MutationInterruptedException.class) but is silently dropped during binding. The class is not removed bysource/androidx.compose.foundation/foundation-android/Transforms/Metadata.xml, yet zero references to it appear in the consumer-builtXamarin.AndroidX.Compose.Foundation.Android.dll.Sibling types in the same
MutatorMutex.ktfile bind correctly:androidx.compose.foundation.MutatorMutexandroidx.compose.foundation.MutatePriorityandroidx.compose.foundation.internal.PlatformOptimizedCancellationException(the parent)androidx.compose.foundation.MutationInterruptedExceptionWhy it matters
MutationInterruptedExceptionis what Compose throws intoawait-able state-mutation APIs (e.g.ScrollState.animateScrollTo,ScrollState.scrollTo,LazyListState.animateScrollToItem,AnchoredDraggableState.anchoredDrag, allScrollable*.scroll(...)paths) when a higher-priority caller wins theMutatorMutexmid-flight — most commonly when the user touches the scroll surface during a programmatic animated scroll, which ejects the running animation.Without the binding, C# callers using
kotlinx.coroutinesinterop have no typed exception to catch and must fall back tocatch (Java.Lang.Throwable), which over-catches and is what most Android-style code-review guidance discourages. With the binding, they could write the natural:This is the same gap referenced in #1456 (the C# scroll-suspend reproduction) and #1440 (binder mangled-name handling) -- this one is more narrowly about a single missing public type.
Reproduction
Disassemble the bound DLL after a consumer build:
Output:
Confirm the class IS in the upstream JAR by extracting
androidx.compose.foundation.foundation-android.aar->classes.jarand running:The class is
public final, has a public no-arg ctor, and extends a Kotlin abstract class that has both apublic (String)ctor and the synthetic(String, int, DefaultConstructorMarker)no-arg helper.Suspected cause
The parent
PlatformOptimizedCancellationExceptiondeclares its no-arg ctor via the Kotlin synthetic-default pattern ((String s, int default, DefaultConstructorMarker marker)+ apublic X()synthetic). The binder typically surfaces only the explicit(String)overload on the parent, so the subclass loses its only chainable base ctor and gets silently dropped by a later validation pass.(Sibling types like
MutatorMutexandMutatePrioritybind fine because their parents have unambiguous accessible base ctors.)Proposed fix
If the root cause is the dropped no-arg base ctor, adding a
<add-node>insource/androidx.compose.foundation/foundation-android/Transforms/Metadata.xmlmay reintroduce the class. Rough shape:Alternatively, restoring the no-arg ctor on the parent (so the subclass binds naturally) would be cleaner if the binder allows surfacing the synthetic.
Workaround
C# callers can still catch the exception, just not by type:
This works but is fragile, ugly, and discoverability is poor.
Environment
net10.0-android36.0Xamarin.AndroidX.Compose.Foundation1.11.1Xamarin.AndroidX.Compose.Foundation.Android1.11.1SuspendBridge->ScrollState.AnimateScrollToAsyncflow.