Reflection lets your code inspect and manipulate itself at runtime. Instead of knowing types, functions, and properties at compile time, reflection lets you discover them dynamically — read class names, call functions by string, inspect annotations, and access private members. It’s the engine behind frameworks like Gson, Room, Dagger, and Retrofit. Most Android developers don’t use reflection directly every day, but understanding it helps you debug framework behaviour, write powerful utilities, and know when reflection is (and isn’t) the right tool. This guide covers Kotlin’s reflection API, practical use cases, and performance trade-offs.


What is Reflection?

Normal code works with types known at compile time. Reflection works with types discovered at runtime:

// Normal code — types known at compile time
val user = User("Alice", 25)
println(user.name)   // compiler knows User has a name property

// Reflection — types discovered at runtime
val kClass = user::class
println(kClass.simpleName)   // "User"

kClass.memberProperties.forEach { prop ->
    println("${prop.name} = ${prop.call(user)}")
}
// name = Alice
// age = 25
// We didn't need to know User's structure at compile time

Setup

Kotlin reflection requires an additional dependency. Basic ::class works without it, but the full API needs kotlin-reflect:

// build.gradle.kts
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-reflect")
}

// Note: kotlin-reflect adds ~2.5 MB to your app
// For Android, consider if you really need it — many frameworks
// use Java reflection (Class<T>) instead of Kotlin reflection (KClass<T>)

Class References — KClass

data class User(val name: String, val age: Int, private val email: String)

// Getting a KClass reference
val kClass1 = User::class              // from type
val kClass2 = user::class              // from instance
val kClass3 = Class.forName("com.example.User").kotlin   // from string

// Basic class information
println(kClass1.simpleName)       // "User"
println(kClass1.qualifiedName)    // "com.example.User"
println(kClass1.isData)           // true
println(kClass1.isSealed)         // false
println(kClass1.isAbstract)       // false
println(kClass1.isFinal)          // true
println(kClass1.visibility)       // PUBLIC

// Java interop
val javaClass: Class<User> = User::class.java       // KClass → Java Class
val backToKotlin: KClass<User> = javaClass.kotlin    // Java Class → KClass

Inspecting class hierarchy

open class Animal(val name: String)
class Dog(name: String, val breed: String) : Animal(name), Serializable

val kClass = Dog::class

// Superclasses and interfaces
println(kClass.supertypes)
// [Animal, Serializable, Any]

// Check relationships
println(kClass.isSubclassOf(Animal::class))   // true
println(kClass.isSubclassOf(Any::class))      // true

// Sealed class subtypes
sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String) : Result()
}

Result::class.sealedSubclasses.forEach {
    println(it.simpleName)   // "Success", "Error"
}

Properties — KProperty

data class User(val name: String, var age: Int, private val email: String)

val user = User("Alice", 25, "alice@example.com")
val kClass = User::class

// All member properties
kClass.memberProperties.forEach { prop ->
    println("${prop.name}: ${prop.returnType}")
}
// name: kotlin.String
// age: kotlin.Int
// email: kotlin.String

// Accessing a specific property
val nameProp = kClass.memberProperties.first { it.name == "name" }
println(nameProp.get(user))   // "Alice"

// Checking mutability
kClass.memberProperties.forEach { prop ->
    val mutable = if (prop is KMutableProperty<*>) "var" else "val"
    println("$mutable ${prop.name}")
}
// val name
// var age
// val email

// Modifying a mutable property
val ageProp = kClass.memberProperties
    .first { it.name == "age" } as KMutableProperty1<User, Int>
ageProp.set(user, 30)
println(user.age)   // 30

Accessing private properties

val emailProp = kClass.memberProperties.first { it.name == "email" }
// emailProp.get(user)   // ❌ throws IllegalCallableAccessException

emailProp.isAccessible = true   // bypass visibility
println(emailProp.get(user))    // "alice@example.com"

// ⚠️ isAccessible = true breaks encapsulation
// Use only for testing, debugging, or framework-level code

Functions — KFunction

class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun multiply(a: Int, b: Int): Int = a * b
    private fun secret(): String = "hidden"
}

val calc = Calculator()
val kClass = Calculator::class

// List all functions
kClass.memberFunctions.forEach { func ->
    println("${func.name}(${func.parameters.drop(1).joinToString { it.name ?: "?" }})")
}
// add(a, b)
// multiply(a, b)
// secret()
// equals(other)
// hashCode()
// toString()

// Calling a function by name
val addFunc = kClass.memberFunctions.first { it.name == "add" }
val result = addFunc.call(calc, 3, 4)   // 7
// First parameter is always the instance (receiver)

// Function metadata
println(addFunc.returnType)            // kotlin.Int
println(addFunc.parameters.size)       // 3 (instance + a + b)
println(addFunc.visibility)            // PUBLIC

// Calling private functions
val secretFunc = kClass.memberFunctions.first { it.name == "secret" }
secretFunc.isAccessible = true
println(secretFunc.call(calc))   // "hidden"

Constructors — KFunction

data class User(val name: String, val age: Int = 0)

val kClass = User::class

// Primary constructor
val primaryConstructor = kClass.primaryConstructor!!
println(primaryConstructor.parameters.map { "${it.name}: ${it.type}" })
// [name: kotlin.String, age: kotlin.Int]

// Check for default values
primaryConstructor.parameters.forEach { param ->
    println("${param.name} — optional: ${param.isOptional}")
}
// name — optional: false
// age — optional: true (has default value)

// Create instance with all parameters
val user1 = primaryConstructor.call("Alice", 25)

// Create instance using parameter names (respects defaults)
val user2 = primaryConstructor.callBy(
    mapOf(primaryConstructor.parameters.first() to "Bob")
)
// user2 = User(name=Bob, age=0) — age uses default value

// All constructors
kClass.constructors.forEach { constructor ->
    println("Constructor: ${constructor.parameters.size} params")
}

Annotations

// Define custom annotations
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)   // must be RUNTIME for reflection
annotation class JsonField(val name: String = "")

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Ignore

// Use annotations
data class User(
    @JsonField("user_name") val name: String,
    @JsonField val age: Int,
    @Ignore val password: String
)

// Read annotations via reflection
val kClass = User::class

kClass.memberProperties.forEach { prop ->
    val jsonField = prop.findAnnotation<JsonField>()
    val ignore = prop.findAnnotation<Ignore>()

    when {
        ignore != null -> println("${prop.name} — IGNORED")
        jsonField != null -> {
            val key = jsonField.name.ifEmpty { prop.name }
            println("${prop.name} — JSON key: $key")
        }
        else -> println("${prop.name} — no annotation")
    }
}
// name — JSON key: user_name
// age — JSON key: age
// password — IGNORED

Building a simple JSON serializer with reflection

fun Any.toJsonString(): String {
    val kClass = this::class
    val properties = kClass.memberProperties
        .filterNot { it.findAnnotation<Ignore>() != null }

    val entries = properties.map { prop ->
        prop.isAccessible = true
        val key = prop.findAnnotation<JsonField>()?.name?.ifEmpty { prop.name } ?: prop.name
        val value = prop.getter.call(this)
        val jsonValue = when (value) {
            is String -> "\"$value\""
            null -> "null"
            else -> value.toString()
        }
        "\"$key\": $jsonValue"
    }

    return "{ ${entries.joinToString(", ")} }"
}

val user = User("Alice", 25, "secret123")
println(user.toJsonString())
// { "user_name": "Alice", "age": 25 }
// password is ignored, name is mapped to user_name

Callable References

Kotlin provides lightweight reflection references using :: syntax. These don’t need the kotlin-reflect library:

// Property reference
data class User(val name: String, val age: Int)

val nameRef = User::name              // KProperty1<User, String>
println(nameRef.get(User("Alice", 25)))   // "Alice"

// Use in higher-order functions
val names = users.map(User::name)     // same as { it.name }
val adults = users.filter { it.age >= 18 }
val sorted = users.sortedBy(User::age)

// Function reference
fun isAdult(user: User): Boolean = user.age >= 18
val adults2 = users.filter(::isAdult)

// Constructor reference
val createUser = ::User               // (String, Int) -> User
val user = createUser("Alice", 25)

// Bound reference — tied to a specific instance
val alice = User("Alice", 25)
val aliceName = alice::name           // () -> String (no User parameter needed)
println(aliceName.get())              // "Alice"

Kotlin Reflection vs Java Reflection

// Kotlin reflection — KClass, KProperty, KFunction
val kClass = User::class
val kProps = kClass.memberProperties          // Kotlin-aware
val kFuncs = kClass.memberFunctions           // includes extensions

// Java reflection — Class, Field, Method
val jClass = User::class.java
val jFields = jClass.declaredFields           // raw JVM fields
val jMethods = jClass.declaredMethods         // raw JVM methods

// Key differences:
// Kotlin reflection understands:
//   - val vs var (KMutableProperty)
//   - data classes (isData)
//   - sealed classes (sealedSubclasses)
//   - default parameter values (isOptional)
//   - nullability (returnType.isMarkedNullable)
//   - companion objects
//   - extension functions

// Java reflection:
//   - Lighter weight (no kotlin-reflect dependency)
//   - Used by most Android frameworks (Gson, Retrofit, Room)
//   - Doesn't understand Kotlin-specific features

// In Android, prefer Java reflection when:
// - Working with frameworks that expect Class<T>
// - You want to avoid the kotlin-reflect library size
// - You don't need Kotlin-specific metadata

// Use Kotlin reflection when:
// - You need to understand val/var, defaults, nullability
// - You're building Kotlin-specific tooling
// - You're inspecting sealed/data classes

Performance Considerations

// Reflection is SLOW compared to direct code
// Direct call: ~1 nanosecond
// Reflection call: ~100-1000 nanoseconds (100x-1000x slower)

// ❌ Don't use reflection in hot paths
fun processItems(items: List<Any>) {
    items.forEach { item ->
        val kClass = item::class   // reflection on every iteration — slow
        kClass.memberProperties.forEach { prop ->
            prop.isAccessible = true
            prop.getter.call(item)   // reflected call on every property — slow
        }
    }
}

// ✅ Cache reflection results
val propertyCache = mutableMapOf<KClass<*>, List<KProperty1<*, *>>>()

fun getCachedProperties(kClass: KClass<*>): List<KProperty1<*, *>> {
    return propertyCache.getOrPut(kClass) {
        kClass.memberProperties.toList().onEach { it.isAccessible = true }
    }
}

// ✅ Use reflection at initialization, not in loops
class MySerializer<T : Any>(private val kClass: KClass<T>) {
    // Cache everything at construction time
    private val properties = kClass.memberProperties.toList()
    private val constructor = kClass.primaryConstructor!!

    fun serialize(instance: T): Map<String, Any?> {
        return properties.associate { prop ->
            prop.name to prop.getter.call(instance)
        }
    }
}

Real Android Patterns

Generic ViewModel factory

class GenericViewModelFactory(
    private val creators: Map<KClass<*>, () -> ViewModel>
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val creator = creators[modelClass.kotlin]
            ?: throw IllegalArgumentException("Unknown ViewModel: ${modelClass.simpleName}")
        return creator() as T
    }
}

// Usage
val factory = GenericViewModelFactory(
    mapOf(
        ArticleViewModel::class to { ArticleViewModel(repository) },
        UserViewModel::class to { UserViewModel(userRepo) }
    )
)

Logging all properties for debugging

fun Any.debugLog(tag: String = "DEBUG") {
    val kClass = this::class
    val props = kClass.memberProperties.joinToString("\n") { prop ->
        try {
            prop.isAccessible = true
            "  ${prop.name} = ${prop.getter.call(this)}"
        } catch (e: Exception) {
            "  ${prop.name} = [inaccessible]"
        }
    }
    Log.d(tag, "${kClass.simpleName} {\n$props\n}")
}

// Usage
user.debugLog()
// D/DEBUG: User {
//   name = Alice
//   age = 25
//   email = alice@example.com
// }

Automatic Parcelable-like mapping

// Convert data class to Bundle and back using reflection
fun <T : Any> T.toBundle(): Bundle {
    val bundle = Bundle()
    val kClass = this::class
    kClass.memberProperties.forEach { prop ->
        prop.isAccessible = true
        val value = prop.getter.call(this)
        when (value) {
            is String -> bundle.putString(prop.name, value)
            is Int -> bundle.putInt(prop.name, value)
            is Boolean -> bundle.putBoolean(prop.name, value)
            is Long -> bundle.putLong(prop.name, value)
            is Float -> bundle.putFloat(prop.name, value)
            is Double -> bundle.putDouble(prop.name, value)
        }
    }
    return bundle
}

inline fun <reified T : Any> Bundle.toDataClass(): T {
    val kClass = T::class
    val constructor = kClass.primaryConstructor!!
    val args = constructor.parameters.associateWith { param ->
        this.get(param.name)
    }
    return constructor.callBy(args)
}

// Usage
val bundle = User("Alice", 25).toBundle()
val user = bundle.toDataClass<User>()

Common Mistakes to Avoid

Mistake 1: Using reflection when direct code works

// ❌ Reflection for something you already know at compile time
val name = user::class.memberProperties
    .first { it.name == "name" }
    .getter.call(user)

// ✅ Just access it directly
val name = user.name

Mistake 2: Forgetting RUNTIME retention on annotations

// ❌ Default retention is RUNTIME in Kotlin, but explicit is safer
@Target(AnnotationTarget.PROPERTY)
annotation class MyAnnotation   // RUNTIME by default — works

// In Java, default is CLASS retention — invisible to reflection!
// Always be explicit:
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)   // ✅ explicit
annotation class MyAnnotation

Mistake 3: Not handling reflection exceptions

// ❌ Crashes if property doesn't exist or is inaccessible
val value = kClass.memberProperties.first { it.name == "missing" }

// ✅ Handle gracefully
val value = kClass.memberProperties.firstOrNull { it.name == "missing" }
if (value != null) {
    value.isAccessible = true
    println(value.getter.call(instance))
}

Mistake 4: Using reflection in ProGuard/R8 without keep rules

// ❌ R8 renames/removes classes and members — reflection breaks
val kClass = Class.forName("com.example.User").kotlin
// After R8: com.example.User might be renamed to "a.b.c"

// ✅ Add ProGuard keep rules for reflected classes
// proguard-rules.pro:
// -keep class com.example.User { *; }
// -keep class com.example.models.** { *; }

// ✅ Or use @Keep annotation
@Keep
data class User(val name: String, val age: Int)

Summary

  • Reflection lets you inspect and manipulate code at runtime — classes, properties, functions, annotations
  • Kotlin reflection requires the kotlin-reflect dependency (~2.5 MB) — basic ::class works without it
  • KClass gives you class metadata — name, superclasses, sealed subclasses, data class status
  • KProperty lets you read/write properties, check val vs var, and access private members
  • KFunction lets you call functions dynamically, inspect parameters, and use default values via callBy
  • Annotations with RUNTIME retention can be read via findAnnotation<T>()
  • Callable references (::property, ::function, ::Constructor) are lightweight and don’t need kotlin-reflect
  • Kotlin reflection understands val/var, nullability, defaults, sealed/data classes — Java reflection doesn’t
  • Reflection is 100x-1000x slower than direct code — cache results and avoid hot paths
  • Use isAccessible = true to access private members — but only for testing or framework code
  • Always add ProGuard/R8 keep rules for classes accessed via reflection
  • Most Android frameworks use Java reflection (Class<T>) — use Kotlin reflection only when you need Kotlin-specific features

Reflection is a powerful tool, but it’s a sharp one. Use it when you genuinely need runtime flexibility — custom serialisation, framework-level tooling, debug utilities, and annotation processing. For everything else, prefer direct code, generics, and reified type parameters. The best reflection code is the code that caches everything upfront and keeps the hot path reflection-free.

Happy coding!