Kotlin Collections: Getting Started
In this tutorial, you’ll learn how to work with Kotlin Collections. You’ll transform data, filter it out, and use different types of collections in Kotlin! By Filip Babić.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin Collections: Getting Started
30 mins
- Getting Started
- What are Collections?
- Collection Types
- Arrays in Kotlin
- Creating An Array
- Working With Arrays
- Lists in Kotlin
- Creating a List
- Mutable Lists
- Sets in Kotlin
- Creating Sets
- Working With Sets
- Maps in Kotlin
- Creating a Map
- Working With a Map
- Concrete Collection Types
- Collection Operators
- Transforming Data
- Filtering and Grouping Data
- Validating Data
- Looking Up Data
- Where To Go From Here
Concrete Collection Types
Kotlin collections have to be interoperable with Java. Because of this, they revolve around the usage of the same concrete types when it comes to each collection type. For example, maps can be HashMap
or a LinkedHashMap
.
Concrete types are a huge topic and one tutorial won’t do them justice. It’s better to check out this link, which briefly explains the various types. Alternatively, check out the official documentation for Java collection types, starting with the List.
Collection Operators
The best feature Kotlin collections offers is the ability to transform between Kotlin collection types using collection operators. You can use them to:
- Transform from a list to a set.
- Turn a list of strings to a list of integers.
- Group the data according to certain conditions.
You’ll learn several usages of the operators including:
- filtering elements.
- looking up data.
- grouping data.
- transforming the elements.
- validating the element.s
Open Operations.kt and add the following code:
data class Product(
val id: Int,
val name: String,
val price: Double
)
class Receipt(
val id: Int,
val seller: Worker,
val products: List<Product>,
val isPaid: Boolean = false
)
class Store(
val receipts: List<Receipt>,
val workers:List<Worker>
)
data class Worker(
val id: Int,
val name: String
)
fun beer() = Product(id = 2, name = "Beer, light, 0.5l", price = 7.5)
fun coffee() = Product(id = 3, name = "Ground coffee 1kg", price = 5.0)
fun bread() = Product(id = 1, name = "Gluten-free bread, 1kg", price = 5.0)
fun main() {
val firstWorker = Worker(id = 1, name = "Filip")
val secondWorker = Worker(id = 2, name = "Chris")
val store = Store(
// 1
receipts = listOf(
Receipt(
//2
id = 1,
seller = firstWorker,
products = listOf(bread(), bread(), bread(), coffee(), beer()),
isPaid = true
),
Receipt(
id = 2,
seller = secondWorker,
products = listOf(coffee(), coffee(), beer(), beer(), beer(), beer(), beer()),
isPaid = false
),
Receipt(
id = 3,
seller = secondWorker,
products = listOf(beer(), beer(), bread()),
isPaid = false
)
),
// 3
workers = listOf(firstWorker, secondWorker)
)
}
Here’s a play-by-play of what’s happening above:
- First, you create a store with a list of receipts from purchases.
- Each receipt has a list of products, whether it’s paid or not, an id and the worker which handled the receipt.
- You then add a list of workers to the store, because someone has to sell the products.
You can transform the data to create something more meaningful than this huge construct.
Transforming Data
Transforming is the process of taking one type and converting it to another. For example, you can transform a String
into an Int
by calling string.length
on it.
In your store example, you need to get all the products you sold. Add the following code to main()
:
val receipts = store.receipts // fetch the receipts
val productsLists = receipts.map { it.products } // List<List<Product>>
println(productsLists)
By using the map operator, you can transform all the receipts collection elements to their respective products. Map iterates over each element of type T and passes it into a provided lambda function. The lambda function returns an element of type R, which in this case, turns each receipt item into a product. What you get back is a List of List of Products, which is fine, but clunky to work with.
You can improve this by using the flatMap operator instead:
val receipts = store.receipts // fetch the receipts
val allProducts = receipts.flatMap { it.products } // List<Product>
println(allProducts)
Flat mapping not only means transforming the elements (a la map) but also flattens them into a single list which is easier to use.
But what if you need all the prices of the products or their sum?
val receipts = store.receipts // fetch the receipts
val allProductsEarnings = receipts.flatMap { it.products }
.map { it.price }
.sumByDouble { it }
println(allProductsEarnings)
You can chain Kotlin collection operators and create really clean and readable transformation operations.
Here, you first flattened and mapped the receipts to products. Then you mapped it to the price, which you ultimately summed. You can also use sumByDouble { it.price }
in place of the map saving you one precious line of code.
Pretty neat, huh? :]
Filtering and Grouping Data
Each store has to keep a record of all the paid and unpaid receipts. You can do this, using filtering and grouping operators, by adding this code to main()
:
// filtering by condition
val paidReceipts = receipts.filter { it.isPaid }
println(paidReceipts)
// grouping values by condition
val paidUnpaid = receipts.partition { it.isPaid }
val (paid, unpaid) = paidUnpaid
println(paid)
println(unpaid)
val groupedByWorker = receipts.groupBy { it.seller } // Map<Worker, List<Receipt>>
println(groupedByWorker)
You can filter out one partition of the results by using filter()
and providing a lambda to filter by. The lambda takes each element T and uses the expression in the curly braces to validate if it should be collected.
partition()
, however, takes the same lambda but returns a pair of elements that match the predicate and elements that don’t. Finally, you can use groupBy()
and provide a value matcher in the lambda, which it will use to compare to other elements of a list and group the ones which match the value with hashCode()
.
Here you used a Worker
to group the values and, in the end, you receive a map holding the list of all the receipts for each of the workers.
Validating Data
Sometime you don’t need to filter data but instead need to check if the data conforms to a condition. For example, you might want to check if all the receipts are paid. You can do that as well:
val areThereNoReceipts = receipts.isEmpty() // also isNotEmpty
val areAllPaid = receipts.all { it.isPaid }
val nonePaid = receipts.none { it.isPaid }
val isAtLeastOnePaid = receipts.any { it.isPaid }
The all()
, none()
and other functions allow you to return a boolean to see if any, all or none of the elements match the given condition.