Kotlin Map.get throws NoSuchElementException when wrapped with Jetpack Compose 'by mutableStateMapOf' (SnapshotStateMap)

Viewed 45

Element accessors on a map should return null on non-existing keys, but instead

myMapOfThings["item3"] throws java.util.NoSuchElementException: Key item3 is missing in the map.

When a Map is initialized (via SnapshotStateMap) in ViewModel with

var myMapOfThings: MutableMap<String, MyType> by mutableStateMapOf(
    "item1" to MyType(),
    "item2" to MyType()
)

All documentation seems to indicate it should behave as a true map

* Create a instance of [MutableMap]<K, V> that is observable and can be snapshot.
* @see mutableMapOf

and as declared for Map in package kotlin.collections

/**
* Returns the value corresponding to the given [key], or `null` if such a key is not present in the map.
*/
public operator fun get(key: K): V?

Is this a bug or am I missing something?

(Build Info for Reference)

kotlin_version = '1.8.0'
kotlin_plugin_version = '1.7.0'
compose_version = '1.2.0'
coroutines_version = '1.6.1'

compileSdk 32
buildToolsVersion = "30.0.3"
minSdk 28
targetSdk 32

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
    jvmTarget = '1.8'
}
1 Answers

You are initializing using delegated property. When you click on by it will be pointing to MapAccesors.kt . Delegated map implementation is a wrapper(overridden) of original map interface. When you use by Delegate property to initialize values it will be pointing to overriden funtion. Here you can see it throws NoSuchElementException if not present.

@kotlin.jvm.JvmName("getOrImplicitDefaultNullable")
@PublishedApi
internal fun <K, V> Map<K, V>.getOrImplicitDefault(key: K): V {
    if (this is MapWithDefault)
        return this.getOrImplicitDefault(key)

    return getOrElseNullable(key, { throw NoSuchElementException("Key $key is missing in the map.") })
}


@kotlin.jvm.JvmName("getVar")
@kotlin.internal.InlineOnly
public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 =
    @Suppress("UNCHECKED_CAST") (getOrImplicitDefault(property.name) as V1)

One way to fix is to initialize with = instead of using by.

var myMapOfThings: MutableMap<String, MyType> = mutableStateMapOf(
    "item1" to MyType(),
    "item2" to MyType()
)
Related