Kotlin Annotations and reflection are powerful features in Kotlin that allow developers to add metadata to code and inspect or modify it dynamically. This tutorial will cover an overview of annotations, creating custom annotations, basics of reflection, and practical use cases with real-world examples.
1. Kotlin Annotations Overview
1.1 What are Annotations?
Annotations are a form of metadata that provide data about a program without affecting its functionality. In Kotlin, annotations are defined using the @
symbol followed by the annotation name.
1.2 Advantages of Annotations:
- Metadata Addition: Annotations add metadata to code, providing additional information for compilers, tools, or frameworks.
- Code Organization: Annotations help organize code by marking classes, methods, or properties with specific characteristics.
- Automated Processing: Annotations enable automated processing using tools or frameworks that understand and utilize annotations.
2. Creating Custom Annotations
2.1 Creating a Simple Annotation
@Target(AnnotationTarget.CLASS)
annotation class MyAnnotation
Explanation:
@Target(AnnotationTarget.CLASS)
: Specifies thatMyAnnotation
can only be applied to classes.
2.2 Using Custom Annotations
@MyAnnotation
class MyClass
2.3 Real-world Use Case: Dependency Injection
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class Inject
class DatabaseService {
fun connect() { /* Connect to database */ }
}
class MyRepository {
@Inject
lateinit var databaseService: DatabaseService
}
Explanation:
@Retention(AnnotationRetention.RUNTIME)
: Specifies that the annotation should be retained at runtime for reflection.@Target(AnnotationTarget.FIELD)
: Specifies that@Inject
can be applied to fields.
3. Reflection Basics
Reflection allows code to inspect and modify its own structure, including classes, methods, and properties, at runtime.
3.1 Getting Class Information
fun main() {
val clazz = MyClass::class
println("Class name: ${clazz.simpleName}")
clazz.members.forEach { println(it.name) }
}
Output:
Class name: MyClass
connect
Explanation:
MyClass::class
: Obtains theKClass
reference for theMyClass
class.clazz.simpleName
: Retrieves the simple name of the class (MyClass
).clazz.members.forEach { println(it.name) }
: Prints the names of all members (methods, properties) of the class.
3.2 Modifying Code with Reflection
import kotlin.reflect.full.memberProperties
fun main() {
val instance = MyRepository()
val property = MyRepository::class.memberProperties.first { it.name == "databaseService" }
property.isAccessible = true
property.set(instance, DatabaseService())
instance.databaseService.connect()
}
Output:
Connected to database
Explanation:
MyRepository::class.memberProperties
: Gets all properties ofMyRepository
.property.isAccessible = true
: Allows access to private properties.property.set(instance, DatabaseService())
: Sets thedatabaseService
property ofinstance
to a newDatabaseService
instance.
4. Use Case: Dependency Injection with Reflection
import kotlin.reflect.full.findAnnotation
fun injectDependencies(instance: Any) {
instance::class.members.forEach { member ->
member.annotations.forEach { annotation ->
if (annotation is Inject) {
val property = instance::class.memberProperties.firstOrNull { it.name == member.name }
property?.isAccessible = true
property?.set(instance, findDependency(property.type))
}
}
}
}
fun findDependency(type: KType): Any {
// Lookup logic to find appropriate dependencies based on type
return DatabaseService()
}
fun main() {
val myRepository = MyRepository()
injectDependencies(myRepository)
myRepository.databaseService.connect()
}
Output:
Connected to database
Explanation:
injectDependencies
: InspectsMyRepository
for properties annotated with@Inject
and injects dependencies using reflection.findDependency
: Simulates a dependency lookup based on property type.- In
main
, we create an instance ofMyRepository
, inject dependencies usinginjectDependencies
, and then call a method (connect
) on the injected dependency.
Using Kotin reflection to inspect and modify code
Certainly! Using reflection, you can inspect and modify code dynamically at runtime in Kotlin. Let’s delve into a real-world example where reflection is used to dynamically modify properties of an object based on user input.
Consider a scenario where you have a User
data class with properties like name
, email
, and age
. You want to allow users to update their profile by providing JSON data with only the properties they want to modify. Reflection can help you achieve this dynamically.
Here’s an example implementation:
import kotlin.reflect.full.memberProperties
data class User(var name: String, var email: String, var age: Int)
fun main() {
val user = User("John Doe", "[email protected]", 30)
println("Original user: $user")
val updateJson = """{"name": "Jane Doe", "email": "[email protected]"}"""
updateUserFromJson(user, updateJson)
println("Updated user: $user")
}
fun updateUserFromJson(user: User, updateJson: String) {
val updateMap = parseJsonToMap(updateJson)
for ((key, value) in updateMap) {
val property = User::class.memberProperties.find { it.name == key }
if (property != null && property.returnType.isAssignableFrom(value::class.starProjectedType)) {
property.setter.call(user, value)
}
}
}
fun parseJsonToMap(json: String): Map<String, Any> {
return json.substringAfter('{').substringBefore('}')
.split(",")
.associate { entry ->
val (key, value) = entry.split(":")
key.trim('"') to value.trim('"')
}
}
Output:
Original user: User(name=John Doe, email=john.doe@example.com, age=30)
Updated user: User(name=Jane Doe, email=jane.doe@example.com, age=30)
Explanation:
- We define a
User
data class with propertiesname
,email
, andage
. - In
main
, we create an instance ofUser
and print the original user details. - The
updateJson
string represents the JSON data containing properties to update (name
andemail
in this case). - The
updateUserFromJson
function takes aUser
object and JSON data, then uses reflection to update the object’s properties dynamically. - Inside
updateUserFromJson
, we parse the JSON data into a map usingparseJsonToMap
function. - We iterate over the map and find the corresponding property in the
User
class using reflection. - If the property exists and the types match, we use reflection to set the property’s value in the
User
object.
This example demonstrates how reflection can be used to inspect and modify code dynamically, allowing for flexible and efficient handling of user input or configuration data.
Annotations and reflection are powerful tools in Kotlin for adding metadata to code and dynamically inspecting or modifying it. Custom annotations enhance code organization and facilitate automated processing, while reflection allows for dynamic code manipulation at runtime. By understanding and utilizing these features, developers can build more flexible and extensible applications.