Which Map Transformation Should I Use?

2024-07-04

Map transformation functions find common usage in Android development. They are part of the Kotlin Standard Library, a library built by JetBrains to provide standard functionality across Kotlin codebases.

Android Weekly Badge

Side Note: Hey, thanks for taking the time to read this blog post! Before you dive in I wanted to let you know I’m looking for my next role. If you have a position you think would be a good fit (skills and experience are on my about page) I’d love to hear about it! Send me an email and I’ll get back to you.

Inside the Standard Library is a package called kotlin.collections, containing the building blocks for different collections. These include Lists, Maps, and Sets.

The collections package also contain the map transformation functions. These functions take the contents of a collection and transform them into another collection containing the transformed state.

Map transformation diagram

Let’s take a look at some examples.

The Map Transformation

The first transformation is the map() function.

  val numbersList = listOf(1, 2, 3)
  numbersList.map { it + 1 }.also(::println) // listOf(2, 3, 4)

  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  numbersMap.map { it.value + 1 }.also(::println) // listOf(2, 3, 4)

  val numbersSet = setOf(1, 2, 3)
  numbersSet.map { it + 1 }.also(::println) // listOf(2, 3, 4)

This function iterates over a collection and applies the transformation to each value within a lambda, before returning a new collection containing the transformed values.

map() is available across different types of collections. The reason for this is because map() is an extension function on the Iterable interface. Most collections implement Iterable, meaning they can make use of the map function.

Map collection types are different. They don’t implement Iterable, instead they possess a separate extension method to provide a map function that iterates over each entry and returns a List of results.

Map Transformations and Null Values

Map transformations come in different forms. Another useful transformation is the mapNotNull() function.

  val numbersList = listOf(1, 2, 3)
  numbersList.mapNotNull { if (it + 1 == 3) null else it + 1  }.also(::println) // listOf(2, 4)

  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)

  numbersMap.mapNotNull { if (it.value + 1 == 3) null else it.value + 1 }.also(::println) // listOf(2, 4)

  val numbersSet = setOf(1, 2, 3)
  numbersSet.mapNotNull { if (it + 1 == 3) null else it + 1 }.also(::println) // listOf(2, 4)

This function acts both as a transformation function and a filter for null values. If the transformation inside the lambda results in null then the value is not added to the new list.

mapNotNull() is available to collections implementing the Iterable interface.

Map types have their own extension function to provide similar functionality. Returning a list of results.

Acquiring an Index with Map Transformations

If you need to know the location of the value within the collection being transformed you can use mapIndexed().

  val numbersList = listOf(1, 2, 3)
  numbersList.mapIndexed { index, number -> number + index + 1 }.also(::println) // listOf(2, 4, 6)

  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  
  numbersMap.asIterable().mapIndexed { index, entry -> entry.value + index + 1 }.also(::println) // listOf(2, 4, 6)

  val numbersSet = setOf(1, 2, 3)
  numbersSet.mapIndexed { index, number -> number + index + 1 }.also(::println) // listOf(2, 4, 6)

Here the location of the value (the index) within the collection is passed alongside the value being transformed. mapIndexed() is available to collections implementing the Iterable interface.

Map types don’t have a mapIndexed() extension function. What you can do though is use the asIterable() extension to wrap the Map inside an Iterable instance. Then you can use mapIndexed() without issue.

If you need to check for null values and also require an index you can also use mapIndexedNotNull().

  val numbersList = listOf(1, 2, 3)
  numbersList.mapIndexedNotNull { index, number -> if (number + 1 == 3) null else number + index + 1 }.also(::println) // listOf(2, 6)
  
  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  
  numbersMap.asIterable().mapIndexedNotNull { index, entry -> if (entry.value + 1 == 3) null else entry.value + index + 1 }.also(::println) // listOf(2, 6)

  val numbersSet = setOf(1, 2, 3)
  numbersSet.mapIndexedNotNull { index, number -> if (number + 1 == 3) null else number + index + 1 }.also(::println) //listOf(2, 6)

mapIndexedNotNull() works similarly to mapNotNull(). It filters away null values within the transformation lambda whilst also passing in the index for the value from the collection. Like other map transformations it exists on all types implementing Iterable.

Map types can use the asIterable() function to gain access to mapIndexedNotNull().

Other Transformations for Map Types

Map types work differently than other collections due to not implementing the Collection or Iterable interfaces. Because of this they have a few of their own transformation functions not available to other types. The first is called mapKeys().

  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  
  numbersMap.mapKeys { entry -> entry.key.capitalize() }.also(::println) // mapOf("One" to 1, "Two" to 2, "Three" to 3)

mapKeys() transforms each key within the map by passing through each Entry of the map. Once all the transformations are complete they are applied to the Map.

The second function is called mapValues().

  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  
  numbersMap.mapValues { entry -> entry.value + 1 }.also(::println) // mapOf("one" to 2, "two" to 3, "three" to 4)

mapValues() works in a similar way. It passes through each Entry of the map and transforms each value. Once all the transformations are complete they are applied to the map.

Passing Map Transformations to a Destination

If you want to pass applied transformations to a different collection other than the source there are a few functions to help.

  val numbersList = listOf(1, 2, 3)
  val numbersDestinationSet = mutableSetOf<Int>()
  numbersList.mapTo(numbersDestinationSet) { it + 1 }
  println(numbersDestinationSet) // setOf(2, 3, 4)

  val numbersSet = setOf(1, 2, 3)
  val numbersDestinationList = mutableListOf<Int>()
  numbersSet.mapTo(numbersDestinationList) { it + 1 }
  println(numbersDestinationList) // listOf(2, 3, 4)

The mapTo functions work similarly to map(). The difference is they write the transformations to the passed in collection. The collection being written doesn’t have to be the same type as the source collection. Useful if you have a usecase where a different collection would be more optimal.

Map types can’t use mapTo(). This is because mapTo() expects to write to a MutableCollection, which Map types don’t inherit from.

There is a MutableMap type, however because it doesn’t inherit from MutableCollection there is no mapTo() extension available.

More Resources

If you’d like to learn more about Kotlin’s transformation methods. I highly recommend this page on collection transformations from the Kotlin language documentation.

As well as covering map transformations it also covers other methods like zipping, association and flattening. These topics are beyond the scope of this post however are nevertheless useful to understand.