Kotlin Coroutines have revolutionized how we handle asynchronous programming in Android and server-side applications. They provide a structured and efficient way to manage background tasks without blocking threads. This in-depth guide explores Coroutines, their execution model, dispatchers, structured concurrency, and best practices with real-world examples. Why Kotlin Coroutines? Traditionally, Java developers relied on Threads, Executors,...
Variance determines how subtyping relationships work in a type hierarchy. There are two key types of variance in Kotlin:
Covariance (Out-projected types): A subtype can replace a supertype when reading values.
Contravariance (In-projected types): A supertype can replace a subtype when writing values.
Function Types and Variance
Kotlin functions can be represented as types, for example:
Kotlin
val func: (String) -> Int = { it.length }
Here, func is a function type that takes a String as input and returns an Int. Function types in Kotlin have two parts:
Parameter type(s) — What the function takes in.
Return type — What the function returns.
The variance in function types applies differently to parameters and return types.
Return types are covariant (out).
Parameter types are contravariant (in).
Now, let’s explore how these apply to Kotlin’s function type variance.
Covariance and Contravariance in Kotlin’s Function Types
In Kotlin, a class or interface can be covariant on one type parameter and contravariant on another. One of the classic examples of this is the Function interface. Let’s take a look at the declaration of the Function1 interface, which represents a one-parameter function:
To make the notation more readable, Kotlin provides an alternative syntax (P) -> R to represent Function1<P, R>. In this syntax, you’ll notice that P (the parameter type) is used only in the in position and is marked with the in keyword, while R (the return type) is used only in the out position and is marked with the out keyword.
This means that the subtyping relationship for the function type is reversed for the first type argument (P) and preserved for the second type argument (R).
For example, let’s say you have a higher-order function called enumerateCats that accepts a lambda function taking a Cat parameter and returning a Number:
Kotlin
funenumerateCats(f: (Cat) -> Number) { ... }
Now, suppose you have a function called getIndex defined in the Animal class that returns an Int. You can pass Animal::getIndex as an argument to enumerateCats:
Kotlin
funAnimal.getIndex(): Int = ...enumerateCats(Animal::getIndex) // This code is legal in Kotlin. Animal is a supertype of Cat, and Int is a subtype of Number
In this case, the Animal::getIndex function is accepted because Animal is a supertype of Cat, and Int is a subtype of Number, the function type’s subtyping relationship allows it.
The function (T) -> R is contravariant on its argument and covariant on its return type
This illustration demonstrates how subtyping works for function types. The arrows indicate the subtyping relationship.
Function Type Variance in Kotlin’s Type System
As you now know, when working with function types in Kotlin, variance is explicitly defined:
This aligns with the natural logic of function type variance.
Conclusion
So in summary,
Covariance (out) applies to return types – A function returning a subtype can be assigned to a function returning a supertype.
Contravariance (in) applies to parameter types – A function taking a supertype can be assigned to a function taking a subtype.
Kotlin enforces these rules to ensure type safety and prevent runtime errors.
Understanding Kotlin’s Function Type Variance helps write flexible, reusable, and type-safe code, especially when designing APIs, lambda functions, and higher-order functions.
Kotlin has a robust type system, but one of the trickiest concepts to grasp is contravariance. If you’ve ever scratched your head wondering how contravariance works and why it’s useful, you’re in the right place. This blog will break it down in simple terms with examples so you can fully understand Kotlin contravariance and use...
Kotlin provides powerful collection functions that make data manipulation intuitive and efficient. Among them, fold and filter stand out as essential tools for transforming and processing collections. In this guide, we’ll explore these functions in depth, providing detailed explanations and practical examples to help you leverage their full potential. Understanding the fold Function in Kotlin The...
Kotlin Coroutines make asynchronous programming simpler and more efficient, but choosing the right dispatcher is crucial for performance and responsiveness. In this guide, we’ll explore Coroutine Dispatchers in Kotlin, focusing on Dispatchers.IO, Dispatchers.Main, Dispatchers.Default, and Dispatchers.Unconfined—when to use each, how they work, and best practices. What Are Coroutine Dispatchers in Kotlin? Coroutine Dispatchers determine the thread...
In modern Android and Kotlin development, handling asynchronous data streams efficiently is crucial. Whether you’re working with API responses, user input events, or real-time updates, Kotlin Flow provides a powerful, coroutine-based solution. In this guide, we’ll explore Kotlin Flow in-depth, covering its concepts, operators, builders, real-world use cases, and best practices. What is Kotlin Flow? Kotlin...
Kotlin’s coroutines have revolutionized asynchronous programming by making concurrency more manageable and readable. But to truly harness their power, understanding Coroutine Scopes is essential. In this guide, we’ll break down what Coroutine Scopes are, why they matter, and how they fit into Kotlin’s concurrency model. What Are Coroutine Scopes? A Coroutine Scope defines the lifecycle...
Kotlin is a powerful and flexible programming language that introduces many advanced features to make development easier. One such feature is Use-Site Variance in Kotlin, which helps handle type variance effectively in generic programming. If you’ve ever struggled with understanding variance in Kotlin, this guide will break it down in a simple, easy-to-understand way.
What is Use-Site Variance in Kotlin?
In Kotlin, variance defines how different types relate to each other in a type hierarchy when dealing with generics. Generics allow you to create flexible and reusable code, but without proper variance handling, type safety issues may arise.
Kotlin provides two types of variance:
Declaration-Site Variance — Defined at the class level using out (covariance) and in (contravariance).
Use-Site Variance — Applied at the point where a generic class is used, rather than where it is declared.
Use-Site Variance is particularly useful when you don’t have control over the generic class declaration but still need to enforce type variance.
When to Use Use-Site Variance in Kotlin?
Use-Site Variance in Kotlin is useful in scenarios where:
You are working with a generic class that doesn’t specify variance at the declaration level.
You need to ensure type safety when passing a generic object to a function.
You want to restrict read and write operations based on the expected type.
Understanding when to use it is important because incorrect usage may lead to type mismatches and compilation errors.
BTW, How does use-site variance work in Kotlin?
Kotlin supports use-site variance, you can specify variance at the use site, which means you can indicate the variance for a specific occurrence of a type parameter, even if it can’t be declared as covariant or contravariant in the class declaration. Let’s break down the concepts and see how use-site works.
In Kotlin, many interfaces, like MutableList, are not covariant or contravariant by default because they can both produce and consume values of the types specified by their type parameters. However, in certain situations, a variable of that type may be used only as a producer or only as a consumer.
Consider the function copyData that copies elements from one collection to another:
Kotlin
fun <T> copyData(source: MutableList<T>, destination: MutableList<T>) {for (item in source) { destination.add(item) }}
In this function, both the source and destination collections have an invariant type. However, the source collection is only used for reading, and the destination collection is only used for writing. In this case, the element types of the collections don’t need to match exactly.
To make this function work with lists of different types, you can introduce a second generic parameter:
Kotlin
fun <T : R, R> copyData(source: MutableList<T>, destination: MutableList<R>) {for (item in source) { destination.add(item) }}
In this modified version, you declare two generic parameters representing the element types in the source and destination lists. The source element type (T) should be a subtype of the elements in the destination list (R).
However, Kotlin provides a more elegant way to express this using use-site variance. If the implementation of a function only calls methods that have the type parameter in the “out” position (as a producer) or only in the “in” position (as a consumer), you can add variance modifiers to the particular usages of the type parameter in the function definition.
For example, you can modify the copyData function as follows:
Kotlin
fun <T> copyData(source: MutableList<outT>, destination: MutableList<T>) {for (item in source) { destination.add(item) }}
In this version, you specify the out modifier for the source parameter, which means it’s a projected (restricted) MutableList. You can only call methods that return the generic type parameter (T) or use it in the “out” position. The compiler prohibits calling methods where the type parameter is used as an argument (“in” position).
When using use-site variance in Kotlin, there are limitations on the methods that can be called on a projected type. If you are using a projected type, you may not be able to call certain methods that require the type parameter to be used as an argument (“in” position) :
Kotlin
val list: MutableList<outNumber> = ...list.add(42) // Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add(element: E): Boolean'
Here, list is declared as a MutableList<out Number>, which is an out-projected type. The out projection restricts the type parameter Number to only be used in the “out” position, meaning it can only be used as a return type or read from. You cannot call the add method because it requires the type parameter to be used as an argument (“in” position).
If you need to call methods that are prohibited by the projection, you should use a regular type instead of a projection. In this case, you can use MutableList<Number> instead of MutableList<out Number>. By using the regular type, you can access all the methods available for that type.
Regarding the concept of using the in modifier, it indicates that in a particular location, the corresponding value acts as a consumer, and the type parameter can be substituted with any of its supertypes. This is similar to the contravariant position in Java’s bounded wildcards.
For example, the copyData function can be rewritten using an in-projection:
Kotlin
fun <T> copyData(source: MutableList<T>, destination: MutableList<inT>) {for (item in source) { destination.add(item) }}
In this version, the destination parameter is projected with the in modifier, indicating that it can consume elements of type T or any of its supertypes. This allows you to copy elements from the source list to a destination list with a broader type.
It’s important to note that use-site variance declarations in Kotlin correspond directly to Java’s bounded wildcards. MutableList<out T> in Kotlin is equivalent to MutableList<? extends T> in Java, while the in-projected MutableList<in T> corresponds to Java’s MutableList<? super T>.
Use-site projections in Kotlin can help widen the range of acceptable types and provide more flexibility when working with generic types, without the need for separate covariant or contravariant interfaces.
Conclusion
Use-Site Variance in Kotlin is a powerful feature that ensures type safety while working with generics. By using out when you only need to retrieve values and in when you only need to pass values, you can effectively manage type variance without modifying class declarations.
Understanding how and when to use Use-Site Variance in Kotlin can help you write cleaner, more flexible, and safer generic code. Start experimenting with it in your Kotlin projects and see how it simplifies handling type variance..!
Kotlin’s Flow API has transformed the way we think about asynchronous data streams. But when your app needs to hold state or broadcast events, two specialized types of Flows shine the brightest: StateFlow and SharedFlow. In this guide, we’ll break down StateFlow and SharedFlow in a simple way. Whether you’re working with Jetpack Compose, MVVM,...
Building an Android app that fetches live weather data is a great way to get hands-on with networking, local storage, and clean architecture principles. In this guide, we’ll build a Weather App in Kotlin using MVVM, Retrofit for API calls, and Room for local caching. It’s a solid project for interviews or real-world use — especially if...