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-reflectdependency (~2.5 MB) — basic::classworks without it KClassgives you class metadata — name, superclasses, sealed subclasses, data class statusKPropertylets you read/write properties, check val vs var, and access private membersKFunctionlets you call functions dynamically, inspect parameters, and use default values viacallBy- Annotations with
RUNTIMEretention can be read viafindAnnotation<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 = trueto 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!
Comments (0)