Unleashing the Power of Generics in Kotlin: A Deep Dive

Sumeet Panchal
4 min readJan 13, 2025

--

Generics in Kotlin are one of the language’s most powerful features, enabling developers to create reusable, flexible, and type-safe code. Whether you’re dealing with collections, designing APIs, or crafting libraries, understanding Kotlin’s generics will help you write better and more maintainable code. In this blog, we’ll explore the magic of generics in Kotlin, dive into real-world examples, and uncover advanced concepts like variance and constraints.

What Are Generics?

Generics allow you to define a class, interface, or function that can operate on different types without sacrificing type safety. Instead of hardcoding a specific type, generics let you write flexible and reusable code that adapts to various data types.

Here’s a quick example:

fun <T> printListItems(list: List<T>) {
for (item in list) {
println(item)
}
}

// Works for any type of List!
printListItems(listOf(1, 2, 3)) // Integers
printListItems<String>(listOf("A", "B", "C")) // Optoinal : Explicit type declaration String

In this example, the function printListItems can handle lists of any type thanks to the generic type parameter <T>.

A Real-World Example: API Response Wrapper

Generics shine in real-world scenarios where flexibility and type safety are critical. One common use case is handling API responses. Let’s build a reusable API response wrapper using generics:

// Generic sealed class for API responses
sealed class ApiResponse<T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error<T>(val message: String, val code: Int? = null) : ApiResponse<T>()
class Loading<T> : ApiResponse<T>()
}

// A generic function to handle API results
fun <T> handleApiResponse(response: ApiResponse<T>) {
when (response) {
is ApiResponse.Success -> {
println("Data received: ${response.data}")
}
is ApiResponse.Error -> {
println("Error occurred: ${response.message} (Code: ${response.code})")
}
is ApiResponse.Loading -> {
println("Loading...")
}
}
}

// Example usage with a User model
data class User(val id: Int, val name: String)

fun main() {
val successResponse = ApiResponse.Success(User(1, "John Doe"))
val errorResponse = ApiResponse.Error<User>("Network Error", 404)
val loadingResponse = ApiResponse.Loading<User>()

handleApiResponse(successResponse) // Outputs: Data received: User(id=1, name=John Doe)
handleApiResponse(errorResponse) // Outputs: Error occurred: Network Error (Code: 404)
handleApiResponse(loadingResponse) // Outputs: Loading...
}

Why Is This Powerful?

  1. Type Safety: The ApiResponse<T> ensures that the type of data (T) is consistent throughout the application.
  2. Reusability: This wrapper can handle any data type, from User to List<Order>.
  3. Scalability: You can easily extend the sealed class to include additional states like Empty or Cached.

Variance in Kotlin: in, out, and Star Projections

Kotlin’s variance annotations — in and out — help you handle type hierarchies safely and flexibly. Let’s explore what these mean:

Covariance (out)

Covariance allows a generic type to be a subtype of another. Use out when you only produce values of a generic type (e.g., for reading purposes).

interface Producer<out T> {
fun produce(): T
}

class StringProducer : Producer<String> {
override fun produce(): String = "Hello, Generics!"
}

fun main() {
val producer: Producer<Any> = StringProducer() // Allowed due to 'out'
println(producer.produce())
}

Contravariance (in)

Contravariance allows a generic type to accept supertypes. Use in when you only consume values of a generic type (e.g., for writing purposes).

interface Consumer<in T> {
fun consume(item: T)
}

class StringConsumer : Consumer<String> {
override fun consume(item: String) {
println("Consumed: $item")
}
}

fun main() {
val consumer: Consumer<String> = StringConsumer()
consumer.consume("Generics are awesome!")
}

Star Projections (*)

Star projections provide a way to work with generic types without specifying a concrete type. They’re useful when the type parameter isn’t relevant to your use case.

fun printListSize(list: List<*>) {
println("List size: ${list.size}")
}

fun main() {
printListSize(listOf(1, 2, 3)) // Works with any type of List
printListSize(listOf("A", "B", "C"))
}

Generic Constraints with where

The where keyword allows you to add multiple constraints to a generic type, making it more flexible and expressive.

Example:

fun <T> copyIfMatches(source: List<T>, destination: MutableList<T>, predicate: (T) -> Boolean)
where T : Number, T : Comparable<T> {
for (item in source) {
if (predicate(item)) {
destination.add(item)
}
}
}

fun main() {
val source = listOf(1, 2, 3, 4)
val destination = mutableListOf<Int>()
copyIfMatches(source, destination) { it > 2 }
println(destination) // Outputs: [3, 4]
}

Here, T is constrained to be both a subtype of Number and Comparable<T>. This ensures T support operations like comparison and numeric calculations.

Generic Constraints: Adding Bounds

Sometimes, you can restrict a generic type to a specific hierarchy. Use bounds (<T : SomeType>) to enforce these constraints.

fun <T : Number> sumNumbers(numbers: List<T>): Double {
return numbers.sumOf { it.toDouble() }
}

fun main() {
println(sumNumbers(listOf(1, 2, 3))) // Works
println(sumNumbers(listOf(1.5, 2.5, 3.5))) // Works
// println(sumNumbers(listOf("A", "B"))) // Error: String is not a Number
}

Best Practices for Using Generics in Kotlin

  1. Avoid Overusing Generics: Generics are powerful but can make the code harder to read if overused.
  2. Leverage Variance: Use in and out to simplify your type of relationships and avoid unnecessary casting.
  3. Add Constraints When Needed: Use bounds to restrict generic types and avoid unexpected behavior.
  4. Document Your Generics: Document how your generic types are intended.

Conclusion

Kotlin’s generics offer a robust framework for writing reusable, type-safe, and flexible code. By understanding concepts like variance, star projections, and constraints, you can unlock the full potential of generics in your projects.

Whether designing an API, working with collections, or building a library, generics provide the tools to create scalable and maintainable solutions. So go ahead, embrace generics, and take your Kotlin skills to the next level! 🚀

What’s your favorite use case for Kotlin generics? Share your thoughts and ideas in the comments below!

--

--

Sumeet Panchal
Sumeet Panchal

Written by Sumeet Panchal

Programming enthusiast specializing in Android and React Native, passionate about crafting intuitive mobile experiences and exploring innovative solutions.

No responses yet